fix(places): say where a result came from, and stop asking questions in the form

Six changes to the place picker and the card behind it, all of them about a
panel saying less and meaning more.

The TREK place index has no switch in the admin panel any more. It decides
whether a search leaves the instance at all, which is a property of the
deployment rather than a taste: an operator pins TREK_PLACES_ENABLED=false in
their compose file, and it cannot be turned off from a browser by whoever holds
an admin account that day. The switch never shipped, so nothing to migrate.

The standing "Using OpenStreetMap" notice above the search box is gone. It was
a sentence about configuration in the middle of a form about a place, and it
answered a question nobody asked while leaving the real one open. Each result
now carries a small mark instead: TREK, OpenStreetMap or Google. It sits on the
row rather than above the list, because a list can be two indexes interleaved
and a heading could only name one of them.

The saved-places filters open across the full width of the row. Two of them sit
side by side, only one can be open at a time, and list names were being cut at
about ten characters for no reason.

Three empty states use the mascot the rest of TREK uses. The place details panel
no longer tells the reader to go and ask an administrator for a Google key, the
saved-places list and the road trip rail no longer explain themselves in two
lines of grey text. That needed one new mood, `sad`: `sleepy` reads as falling
asleep, which is a different thing from finding nothing.

The Kind of stop field is out of the place form. It is a road trip concept, and
the road trip view already sets it from the stop's own number, where the choice
is next to what it changes.
This commit is contained in:
Maurice
2026-09-08 17:06:09 +02:00
committed by Maurice
parent 1761f2bcd5
commit 4a88dbcbcf
92 changed files with 283 additions and 557 deletions
-2
View File
@@ -661,8 +661,6 @@ export const adminApi = {
updateBagTracking: (enabled: boolean) => apiClient.put('/admin/bag-tracking', { enabled }).then(r => r.data),
getPlacesPhotos: () => apiClient.get('/admin/places-photos').then(r => r.data),
updatePlacesPhotos: (enabled: boolean) => apiClient.put('/admin/places-photos', { enabled }).then(r => r.data),
getTrekPlaces: () => apiClient.get('/admin/trek-places').then(r => r.data),
updateTrekPlaces: (enabled: boolean) => apiClient.put('/admin/trek-places', { enabled }).then(r => r.data),
getPlaceShadow: () => apiClient.get('/admin/place-shadow').then(r => r.data),
updatePlaceShadow: (enabled: boolean) => apiClient.put('/admin/place-shadow', { enabled }).then(r => r.data),
getPlacesAutocomplete: () => apiClient.get('/admin/places-autocomplete').then(r => r.data),
@@ -5,6 +5,7 @@ import { collectionsApi } from '../../api/collections'
import { STATUS_META, STATUS_ORDER } from '../../pages/collections/collectionsModel'
import type { CollectionPlace, CollectionStatus } from '@trek/shared'
import type { TranslationFn } from '../../types'
import EmptyState from '../shared/EmptyState'
interface LocationBias {
low: { lat: number; lng: number }
@@ -53,7 +54,16 @@ async function loadSavedPlaces(ids: number[]): Promise<CollectionPlace[]> {
return merged
}
/** Compact click-away dropdown (Tailwind — this panel lives outside .trek-dash). */
/**
* Compact click-away dropdown (Tailwind, this panel lives outside .trek-dash).
*
* The panel is positioned against the FILTER ROW rather than against this
* button, so it opens across the full width of the row instead of the half its
* own trigger occupies. The two filters sit side by side and only one can be
* open at a time, so the wider panel costs nothing and stops list names being
* truncated at roughly ten characters. The row carries the `relative` this
* needs; there is no other call site.
*/
function FilterDropdown({ current, options, onSelect, lead }: {
current: string | number
options: Opt[]
@@ -72,7 +82,7 @@ function FilterDropdown({ current, options, onSelect, lead }: {
}, [open])
const cur = options.find(o => o.key === current) ?? options[0]
return (
<div className="relative min-w-0 flex-1" ref={ref}>
<div className="min-w-0 flex-1" ref={ref}>
<button type="button" onClick={() => setOpen(o => !o)} aria-haspopup="listbox" aria-expanded={open}
className={`w-full flex items-center gap-1.5 px-2.5 py-1.5 rounded-lg border bg-surface-input text-[12px] font-medium text-content-secondary transition-colors ${open ? 'border-accent' : 'border-edge hover:bg-surface-hover'}`}>
<span className="shrink-0 text-content-faint">{cur.icon ?? lead}</span>
@@ -80,7 +90,7 @@ function FilterDropdown({ current, options, onSelect, lead }: {
<ChevronDown size={13} className="shrink-0 text-content-faint" />
</button>
{open && (
<div role="listbox" className="absolute z-30 left-0 right-0 mt-1 max-h-[240px] overflow-y-auto p-1 rounded-xl border border-edge bg-surface-card shadow-lg flex flex-col gap-0.5">
<div role="listbox" className="absolute z-30 top-full left-0 right-0 mt-1 max-h-[240px] overflow-y-auto p-1 rounded-xl border border-edge bg-surface-card shadow-lg flex flex-col gap-0.5">
{options.map(o => (
<button key={o.key} type="button" role="option" aria-selected={o.key === current} onClick={() => { onSelect(o.key); setOpen(false) }}
className={`flex items-center gap-2 px-2 py-1.5 rounded-lg text-[12.5px] text-left transition-colors hover:bg-surface-hover ${o.key === current ? 'text-content font-semibold' : 'text-content-secondary'}`}>
@@ -189,7 +199,7 @@ export default function CollectionPicker({ bias, onSelect, t }: CollectionPicker
/>
</div>
{lists.length > 0 && (
<div className="flex gap-2">
<div className="relative flex gap-2">
<FilterDropdown current={listFilter} options={listOpts} onSelect={k => setListFilter(k as number | 'all')} lead={<Layers size={13} />} />
<FilterDropdown current={statusFilter} options={statusOpts} onSelect={k => setStatusFilter(k as CollectionStatus | 'all')} lead={<Bookmark size={13} />} />
</div>
@@ -201,7 +211,13 @@ export default function CollectionPicker({ bias, onSelect, t }: CollectionPicker
<Loader2 size={18} className="animate-spin" />
</div>
) : visible.length === 0 ? (
<p className="text-center text-[12px] text-content-faint py-10 px-3">{t('collections.picker.empty')}</p>
<EmptyState
scene="search"
title={t('collections.picker.empty')}
size={84}
surface="var(--bg-secondary)"
compact
/>
) : (
<div className="flex flex-col gap-1">
{page.map(place => (
@@ -467,41 +467,33 @@ describe('PlaceDetailsColumn — hours and rating', () => {
})
})
/**
* FE-PDC-027..029 — the nudge towards a Google key.
* FE-PDC-027 — nothing came back for this place.
*
* Deliberately narrow: it appears only when the free sources came up empty AND
* no key is configured. On an instance with a key there is nothing to suggest,
* and on a place the free sources described it would read as an advert.
* It used to be a line of grey text plus, when no Google key was set, a panel
* telling the reader to go and ask an administrator for one. The mascot says the
* same thing in the shape every other empty state in TREK uses, and whose key is
* missing is not this panel's business.
*/
describe('PlaceDetailsColumn — no Google key', () => {
const empty = () => placeEnrichment.mockResolvedValue({ photos: [], facts: [], description: null })
describe('PlaceDetailsColumn — nothing found', () => {
it('FE-PDC-027: shows the empty state once, and no advice about API keys', async () => {
placeEnrichment.mockResolvedValue({ photos: [], facts: [], description: null })
renderColumn()
it('FE-PDC-027: suggests a key when nothing was found and none is set', async () => {
empty()
renderColumn({ hasMapsKey: false })
expect(await screen.findByText('places.details.noKeyTitle')).toBeInTheDocument()
expect(screen.getByText('places.details.noKeyHint')).toBeInTheDocument()
})
it('FE-PDC-028: stays quiet when a key is already configured', async () => {
empty()
renderColumn({ hasMapsKey: true })
await screen.findByText('places.details.nothing')
expect(await screen.findByText('places.details.nothing')).toBeInTheDocument()
expect(screen.queryByText('places.details.noKeyTitle')).not.toBeInTheDocument()
expect(screen.queryByText('places.details.noKeyHint')).not.toBeInTheDocument()
})
it('FE-PDC-029: stays quiet when the free sources did find something', async () => {
it('FE-PDC-028: stays quiet when the free sources did find something', async () => {
placeEnrichment.mockResolvedValue({
photos: [],
facts: [],
description: { text: 'Ein Museum.', source: 'wikipedia', sourceUrl: null, license: 'CC BY-SA 4.0' },
})
renderColumn({ hasMapsKey: false })
renderColumn()
await screen.findByText('Ein Museum.')
expect(screen.queryByText('places.details.noKeyTitle')).not.toBeInTheDocument()
expect(screen.queryByText('places.details.nothing')).not.toBeInTheDocument()
})
})
/**
@@ -10,13 +10,11 @@ import {
ChevronUp,
Clock,
ExternalLink,
ImageOff,
Landmark,
Leaf,
Loader2,
ScrollText,
ShoppingBag,
Sparkles,
Sprout,
Star,
Sun,
@@ -27,6 +25,7 @@ import { mapsApi } from '../../api/client'
import { resolveOpenNow, resolvePlaceTimeZone, placeWeekdayIndex } from './placeOpenState'
import { convertHoursLine, isUnknownHoursLine, splitHoursLine } from './placeHoursFormat'
import { safeHttpUrl } from '../../utils/safeUrl'
import EmptyState from '../shared/EmptyState'
import type { TranslationFn } from '../../types'
/** The place the column is describing. Null while nothing is selected. */
@@ -53,7 +52,6 @@ interface PlaceDetailsColumnProps {
/** For grouping the rating count's digits. */
locale?: string
/** False on an instance with no Google key, which is most of them. */
hasMapsKey?: boolean
t: TranslationFn
}
@@ -119,7 +117,6 @@ export default function PlaceDetailsColumn({
language,
timeFormat = '24h',
locale = 'en-US',
hasMapsKey = false,
t,
}: PlaceDetailsColumnProps): React.ReactElement {
const [data, setData] = useState<MapsPlaceEnrichmentResult | null>(null)
@@ -223,17 +220,20 @@ export default function PlaceDetailsColumn({
<p className="text-caption text-content-muted">{t('places.details.disabled')}</p>
)}
{/* The mascot rather than a line of grey text and, under it, a paragraph
telling the reader to go and ask an administrator for a Google key.
Nothing came back for this place, which is worth saying once and
plainly; whose key is missing is not this panel's business, and on a
place the free sources describe fine it read as an advert. */}
{isEmpty && (
<div className="space-y-2">
<div className="flex items-center gap-2 text-caption text-content-muted">
<ImageOff className="w-4 h-4 shrink-0" />
{t('places.details.nothing')}
</div>
{/* Only when both are true. With a key configured there is nothing
to suggest, and on a place the free sources DID describe the
suggestion would be an advert. */}
{!hasMapsKey && <NoKeyHint t={t} />}
</div>
<EmptyState
scene="search"
mood="sad"
title={t('places.details.nothing')}
size={92}
surface="var(--bg-secondary)"
compact
/>
)}
{selection && state === 'ready' && !data?.disabled && !isEmpty && (
@@ -462,19 +462,6 @@ function PhotoCredit({ photo }: { photo: PlacePhotoCandidate }): React.ReactElem
* It also names who to ask: the key is an instance-wide setting, so on most
* installs the person reading this cannot act on it themselves.
*/
function NoKeyHint({ t }: { t: TranslationFn }): React.ReactElement {
return (
<div className="rounded-xl border border-accent/25 bg-accent-subtle p-3">
<div className="flex items-center gap-2">
<span className="flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-accent text-accent-text">
<Sparkles className="h-3 w-3" />
</span>
<p className="text-caption font-semibold text-content">{t('places.details.noKeyTitle')}</p>
</div>
<p className="mt-2 text-caption leading-relaxed text-content-secondary">{t('places.details.noKeyHint')}</p>
</div>
)
}
/**
* The star rating, as stars.
@@ -349,10 +349,55 @@ describe('PlaceFormModal', () => {
delete window.__addToast;
});
it('FE-PLANNER-PLACEFORM-022: hasMapsKey=false shows OSM active message', () => {
// hasMapsKey is false by default in beforeEach
it('FE-PLANNER-PLACEFORM-022: no standing notice about which index is in use', () => {
// It used to say "Using OpenStreetMap" above the box whenever no Google key
// was set, which was a sentence about configuration in the middle of a form
// about a place. Each result names its own source now, which is the question
// people actually had.
render(<PlaceFormModal {...defaultProps} />);
expect(screen.getByText(/OpenStreetMap/i)).toBeInTheDocument();
expect(screen.queryByText(/Using OpenStreetMap/i)).not.toBeInTheDocument();
});
it('FE-PLANNER-PLACEFORM-022b: a suggestion says which index answered', async () => {
const user = userEvent.setup();
server.use(
http.post('/api/maps/autocomplete', () =>
HttpResponse.json({
suggestions: [{ placeId: 'gers:abc', mainText: 'Eiffel Tower', secondaryText: 'Paris, France' }],
source: 'trek-places',
}),
),
);
render(<PlaceFormModal {...defaultProps} />);
await user.type(screen.getByPlaceholderText('Search places...'), 'Eiffel');
expect(await screen.findByText('TREK')).toBeInTheDocument();
});
it('FE-PLANNER-PLACEFORM-022c: an interleaved result list marks each row with its own index', async () => {
// The whole point of putting it on the row: with both indexes answering,
// a label above the list could only name one of them. The index marks its
// own rows, so the unmarked one is OpenStreetMap by elimination.
const user = userEvent.setup();
server.use(
http.post('/api/maps/search', () =>
HttpResponse.json({
places: [
{ name: 'Eiffel Tower', address: 'Paris', lat: '48.85', lng: '2.29', source: 'trek-places' },
{ name: 'Champ de Mars', address: 'Paris', lat: '48.85', lng: '2.29' },
],
source: 'trek-places+openstreetmap',
}),
),
);
render(<PlaceFormModal {...defaultProps} />);
await user.type(screen.getByPlaceholderText('Search places...'), 'Eiffel');
await user.keyboard('{Enter}');
expect(await screen.findByText('TREK')).toBeInTheDocument();
expect(await screen.findByText('OpenStreetMap')).toBeInTheDocument();
});
// ── Category ─────────────────────────────────────────────────────────────────
@@ -1,7 +1,6 @@
import { useState, useEffect, useRef, useMemo, useCallback } from 'react'
import Modal from '../shared/Modal'
import type { RoadtripStopType } from '@trek/shared'
import { STOP_KINDS } from '../Roadtrip/stopKinds'
import CustomSelect from '../shared/CustomSelect'
import NoteFormatToolbar from '../shared/NoteFormatToolbar'
import { mapsApi } from '../../api/client'
@@ -56,6 +55,49 @@ interface PlaceFormModalProps {
}
/**
* Which index a search result came from, as a short mark beside it.
*
* A place list can be two indexes interleaved, so the source belongs on the row
* rather than above the list: with the TREK index and OpenStreetMap answering
* together, "one of these came from somewhere" is not an answer anyone can use.
*
* The three names are proper nouns, so they are not translated, and that is also
* why there is no fourth: a source without a name people already know would need
* a string in 23 languages to say less than nothing.
*/
const SOURCE_LABELS: Record<string, string> = {
'trek-places': 'TREK',
openstreetmap: 'OpenStreetMap',
nominatim: 'OpenStreetMap',
google: 'Google',
}
/**
* The label for one row.
*
* A place carries its own source when the index that produced it says so, which
* is what makes an interleaved list readable. Everything else falls back to what
* answered the call: Google never marks its places, and a merged list marks only
* the index side, so an unmarked row in one is OpenStreetMap by elimination.
*/
function sourceLabelFor(place: unknown, listSource: string): string | null {
const own = (place as { source?: unknown } | null)?.source
if (typeof own === 'string' && own) return SOURCE_LABELS[own] ?? null
if (listSource.includes('openstreetmap')) return SOURCE_LABELS.openstreetmap
return SOURCE_LABELS[listSource] ?? null
}
/** The mark itself. Quiet on purpose: it answers a question, it does not advertise. */
function SourceBadge({ label }: { label: string | null }) {
if (!label) return null
return (
<span className="shrink-0 rounded-md border border-edge bg-surface-secondary px-1.5 py-0.5 text-[10px] font-medium text-content-faint">
{label}
</span>
)
}
/** Place create/edit form state: maps search + Google-URL resolve + autocomplete,
* category creation, file attachments and submit. Keeps PlaceFormModal a thin
* render over the form fields. */
@@ -92,10 +134,11 @@ function usePlaceFormModal(props: PlaceFormModalProps) {
} = props
// Hidden while the addon is off, because the kinds only mean anything to the road trip
// rail: on an instance without it they would be six labels that change nothing.
const roadtripEnabled = useAddonStore(s => s.isEnabled('roadtrip'))
const [form, setForm] = useState(DEFAULT_FORM)
const [mapsSearch, setMapsSearch] = useState('')
const [mapsResults, setMapsResults] = useState([])
/** What answered the last full search. Only a fallback: a merged list carries the source per place. */
const [searchSource, setSearchSource] = useState<string>('')
/**
* What produced the list currently on screen, kept for the shadow log: the
* query as typed and the provider the envelope named. A ref rather than
@@ -119,6 +162,10 @@ function usePlaceFormModal(props: PlaceFormModalProps) {
const fileRef = useRef(null)
const searchInputRef = useRef<HTMLInputElement>(null)
const [acSuggestions, setAcSuggestions] = useState<{ placeId: string; mainText: string; secondaryText: string }[]>([])
// Which index answered the last keystroke. One call is served by one source,
// so the whole list carries it; the search below is per place, because that
// list can be two indexes interleaved.
const [acSource, setAcSource] = useState<string>('')
const [acHighlight, setAcHighlight] = useState(-1)
const acDebounceRef = useRef<ReturnType<typeof setTimeout> | null>(null)
const acAbortRef = useRef<AbortController | null>(null)
@@ -255,6 +302,7 @@ function usePlaceFormModal(props: PlaceFormModalProps) {
const result = await mapsApi.autocomplete(query, language, locationBias, controller.signal, placesSessionRef.current.current())
acMetaRef.current = { query, source: result.source || 'unknown' }
setAcSuggestions(result.suggestions || [])
setAcSource(result.source || '')
setAcHighlight(-1)
} catch (err: unknown) {
if (err instanceof Error && err.name === 'AbortError') return
@@ -315,6 +363,7 @@ function usePlaceFormModal(props: PlaceFormModalProps) {
const result = await mapsApi.search(mapsSearch, language, locationBiasPoint)
searchMetaRef.current = { query: mapsSearch.trim(), source: result.source || 'unknown' }
setMapsResults(result.places || [])
setSearchSource(result.source || '')
} catch (err: unknown) {
toast.error(getApiErrorMessage(err, t('places.mapsSearchError')))
} finally {
@@ -569,7 +618,6 @@ function usePlaceFormModal(props: PlaceFormModalProps) {
dayAssignments,
isMobile,
collectionsEnabled,
roadtripEnabled,
form,
setForm,
mapsSearch,
@@ -589,6 +637,8 @@ function usePlaceFormModal(props: PlaceFormModalProps) {
fileRef,
acSuggestions,
setAcSuggestions,
acSource,
searchSource,
acHighlight,
setAcHighlight,
acDebounceRef,
@@ -642,7 +692,6 @@ export default function PlaceFormModal(props: PlaceFormModalProps) {
dayAssignments,
isMobile,
collectionsEnabled,
roadtripEnabled,
form,
setForm,
mapsSearch,
@@ -662,6 +711,8 @@ export default function PlaceFormModal(props: PlaceFormModalProps) {
fileRef,
acSuggestions,
setAcSuggestions,
acSource,
searchSource,
acHighlight,
setAcHighlight,
acDebounceRef,
@@ -743,18 +794,12 @@ export default function PlaceFormModal(props: PlaceFormModalProps) {
language={language}
timeFormat={S.timeFormat}
locale={S.locale}
hasMapsKey={S.hasMapsKey}
t={t}
/>
)}
<form onSubmit={handleSubmit} className={twoColumn || showDetails ? 'flex-1 min-w-0 space-y-3' : 'space-y-3'} onPaste={handlePaste}>
{/* Place Search */}
<div className="bg-surface-secondary rounded-xl p-3 border border-edge">
{!hasMapsKey && (
<p className="mb-2 text-xs text-content-faint">
{t('places.osmActive')}
</p>
)}
<div className="relative">
<div className="flex gap-2">
<input
@@ -795,10 +840,15 @@ export default function PlaceFormModal(props: PlaceFormModalProps) {
idx === acHighlight ? 'bg-surface-tertiary' : 'hover:bg-surface-hover'
}`}
>
<div className="font-medium text-sm">{s.mainText}</div>
{s.secondaryText && (
<div className="text-xs text-content-muted truncate">{s.secondaryText}</div>
)}
<div className="flex items-center gap-2">
<div className="min-w-0 flex-1">
<div className="font-medium text-sm truncate">{s.mainText}</div>
{s.secondaryText && (
<div className="text-xs text-content-muted truncate">{s.secondaryText}</div>
)}
</div>
<SourceBadge label={sourceLabelFor(s, acSource)} />
</div>
</button>
))}
</div>
@@ -815,8 +865,13 @@ export default function PlaceFormModal(props: PlaceFormModalProps) {
onClick={() => handleSelectMapsResult(result, { mode: 'search', rank: idx, count: mapsResults.length })}
className="w-full text-left px-3 py-2 hover:bg-surface-hover border-b border-edge-faint last:border-0"
>
<div className="font-medium text-sm">{result.name}</div>
<div className="text-xs text-content-muted truncate">{result.address}</div>
<div className="flex items-center gap-2">
<div className="min-w-0 flex-1">
<div className="font-medium text-sm truncate">{result.name}</div>
<div className="text-xs text-content-muted truncate">{result.address}</div>
</div>
<SourceBadge label={sourceLabelFor(result, searchSource)} />
</div>
</button>
))}
</div>
@@ -914,30 +969,6 @@ export default function PlaceFormModal(props: PlaceFormModalProps) {
</div>
</div>
{/* What kind of stop this is on a drive.
Under the category rather than beside it, because the two answer different
questions: a category is the traveller's own label, editable and shared across
the whole instance, while this is a fact about the place. Refuelling is not a
taste. Shown always, not only in road trip mode: a fuel stop added on a Tuesday
is still a fuel stop when the mode is switched on later, and this is the only
place in the web UI where the kind can be taken off again. */}
{roadtripEnabled ? (
<div>
<label className="block text-sm font-medium text-content-secondary mb-1">{t('roadtrip.stop.kind')}</label>
<CustomSelect
value={form.stop_type ?? ''}
onChange={value => handleChange('stop_type', String(value) as RoadtripStopType | '')}
placeholder={t('roadtrip.stop.none')}
options={[
{ value: '', label: t('roadtrip.stop.none') },
...STOP_KINDS.map(k => ({ value: k.key, label: t(k.labelKey) })),
]}
size="sm"
/>
<p className="mt-1 text-caption text-content-faint">{t('roadtrip.stop.kindHelp')}</p>
</div>
) : null}
{/* Category */}
<div>
<label className="block text-sm font-medium text-content-secondary mb-1">{t('places.formCategory')}</label>
@@ -168,11 +168,14 @@ describe('RoadtripSidebar', () => {
expect(screen.getByText('Hamburg').closest('button')).not.toHaveAttribute('aria-current')
})
it('FE-ROADTRIP-SIDEBAR-009: a trip with nothing to drive explains itself', () => {
it('FE-ROADTRIP-SIDEBAR-009: a trip with nothing to drive shows the empty state, not a zeroed rail', () => {
// Nothing to total up, so no "0 km" standing over an empty list. The mascot
// is the shape every other empty state in TREK uses.
wrap(<RoadtripSidebar routes={routes({ days: [], totalDistance: 0, totalDuration: 0, totalStops: 0 })} />)
expect(screen.getByRole('heading', { level: 3 })).toBeInTheDocument()
expect(screen.getByText('No route yet')).toBeInTheDocument()
expect(screen.queryByText('Hamburg')).not.toBeInTheDocument()
expect(screen.queryByText(/0 km/)).not.toBeInTheDocument()
})
it('FE-ROADTRIP-SIDEBAR-010: each day carries its own distance and time', () => {
@@ -1,6 +1,6 @@
import React, { useState } from 'react'
import {
MapPin, CarFront, Footprints, Bike, Zap, AlertTriangle, Moon,
CarFront, Footprints, Bike, Zap, AlertTriangle, Moon,
ParkingSquare, Shuffle, Fuel, Clock, Spline,
type LucideIcon,
} from 'lucide-react'
@@ -18,6 +18,7 @@ import type { QuietDay, RoadtripDay, RoadtripRoutes, RoadtripStop } from './useR
import type { RouteVia } from '../../types'
import { FS } from './typeScale'
import type { RouteSegment } from '../../types'
import EmptyState from '../shared/EmptyState'
interface RoadtripSidebarProps {
/** Legs and totals for the whole trip, computed once in the planner hook. */
@@ -1045,11 +1046,17 @@ export default function RoadtripSidebar({
if (routes.days.length === 0) {
return (
<div className="min-h-0 flex-1 overflow-y-auto">
<div className="px-5 py-8 text-center">
<MapPin size={18} className="mx-auto mb-2 text-content-faint" aria-hidden />
<h3 className="font-semibold text-content" style={{ fontSize: FS.name }}>{t('roadtrip.empty.title')}</h3>
<p className="mt-1 text-content-muted" style={{ fontSize: FS.meta }}>{t('roadtrip.empty.body')}</p>
</div>
{/* The mascot rather than a pin and two lines of instructions. The rail
is empty because the trip has no days with places yet, which is a
state, not a task list: the same look every other empty state in TREK
has. */}
<EmptyState
scene="transport"
mood="sad"
title={t('roadtrip.empty.title')}
size={96}
surface="var(--bg-secondary)"
/>
</div>
)
}
+12 -1
View File
@@ -37,7 +37,7 @@ export type TrekScene =
| 'search'
| 'tasks'
export type TrekMood = 'default' | 'happy' | 'sleepy' | 'confused' | 'error'
export type TrekMood = 'default' | 'happy' | 'sad' | 'sleepy' | 'confused' | 'error'
const MARK =
'M 855.636719 699.203125 L 222.246094 699.203125 C 197.679688 699.203125 179.90625 675.75 186.539062 652.101562 L 360.429688 32.390625 C 364.921875 16.386719 379.511719 5.328125 396.132812 5.328125 L 1029.527344 5.328125 C 1054.089844 5.328125 1071.867188 28.777344 1065.230469 52.429688 L 891.339844 672.136719 C 886.851562 688.140625 872.257812 699.203125 855.636719 699.203125 Z M 444.238281 1166.980469 L 533.773438 847.898438 C 540.410156 824.246094 522.632812 800.796875 498.070312 800.796875 L 172.472656 800.796875 C 155.851562 800.796875 141.261719 811.855469 136.769531 827.859375 L 47.234375 1146.941406 C 40.597656 1170.59375 58.375 1194.042969 82.9375 1194.042969 L 408.535156 1194.042969 C 425.15625 1194.042969 439.75 1182.984375 444.238281 1166.980469 Z M 609.003906 827.859375 L 435.113281 1447.570312 C 428.476562 1471.21875 446.253906 1494.671875 470.816406 1494.671875 L 1104.210938 1494.671875 C 1120.832031 1494.671875 1135.421875 1483.609375 1139.914062 1467.605469 L 1313.804688 847.898438 C 1320.441406 824.246094 1302.664062 800.796875 1278.101562 800.796875 L 644.707031 800.796875 C 628.085938 800.796875 613.492188 811.855469 609.003906 827.859375 Z M 1056.105469 333.019531 L 966.570312 652.101562 C 959.933594 675.75 977.710938 699.203125 1002.273438 699.203125 L 1327.871094 699.203125 C 1344.492188 699.203125 1359.085938 688.140625 1363.574219 672.136719 L 1453.109375 353.054688 C 1459.746094 329.40625 1441.96875 305.953125 1417.40625 305.953125 L 1091.808594 305.953125 C 1075.1875 305.953125 1060.597656 317.015625 1056.105469 333.019531 Z'
@@ -110,6 +110,17 @@ function Eyes({ mood }: { mood: TrekMood }) {
</g>
)
}
if (mood === 'sad') {
// The mirror of `happy`: the same arcs bent the other way. `sleepy` is a
// shallower dip and reads as eyes falling shut, which is a different thing
// from disappointed.
return (
<g fill="none" stroke="var(--m-bg)" strokeWidth="1.8" strokeLinecap="round">
<path d="M33.7 32 Q36 34.8 38.3 32" />
<path d="M42.7 32 Q45 34.8 47.3 32" />
</g>
)
}
if (mood === 'sleepy') {
return (
<g fill="none" stroke="var(--m-bg)" strokeWidth="1.8" strokeLinecap="round">
@@ -494,50 +494,15 @@ describe('AdminSettingsTab', () => {
expect(admin.setShowRotateJwtModal).toHaveBeenCalledWith(true);
});
describe('FE-ADMSET-041: the index switch', () => {
// The one control on this card about egress rather than about spending:
// with the index on, every search and every keystroke in the place box
// leaves the instance. The flag was read by six call paths and written by
// nothing, so before this there was no way to turn it off short of an
// INSERT against app_settings by hand.
const switchFor = () =>
screen.getByRole('button', { name: /trek place index/i });
it('FE-ADMSET-041: the index has no switch, because turning it off is a deployment decision', () => {
// It decides whether a search leaves the instance at all, so it belongs in
// the compose file rather than in a browser: TREK_PLACES_ENABLED=false.
// A card that offered the choice here also had to explain what answers
// instead, which is a paragraph about configuration inside a card about a
// data source.
renderTab();
it('turns the index off and tells the server', async () => {
let sent: unknown = null;
server.use(
http.put('/api/admin/trek-places', async ({ request }) => {
sent = await request.json();
return HttpResponse.json({ enabled: false });
}),
);
const admin = renderTab({ trekPlacesEnabled: true });
expect(switchFor()).toHaveAttribute('aria-pressed', 'true');
fireEvent.click(switchFor());
expect(admin.setTrekPlacesEnabledState).toHaveBeenCalledWith(false);
await waitFor(() => expect(sent).toEqual({ enabled: false }));
});
it('rolls back when the save fails, instead of showing a state the server never took', async () => {
server.use(http.put('/api/admin/trek-places', () => HttpResponse.error()));
const admin = renderTab({ trekPlacesEnabled: true });
fireEvent.click(switchFor());
await waitFor(() => expect(admin.setTrekPlacesEnabledState).toHaveBeenLastCalledWith(true));
});
it('says what answers instead, but only while it is off', () => {
renderTab({ trekPlacesEnabled: false });
expect(screen.getByText(/place search uses openstreetmap/i)).toBeInTheDocument();
expect(switchFor()).toHaveAttribute('aria-pressed', 'false');
});
it('stays quiet about the fallback while the index is on', () => {
renderTab({ trekPlacesEnabled: true });
expect(screen.queryByText(/place search uses openstreetmap/i)).not.toBeInTheDocument();
});
expect(screen.queryByRole('button', { name: /trek place index/i })).not.toBeInTheDocument();
expect(screen.queryByText(/place search uses openstreetmap/i)).not.toBeInTheDocument();
});
});
+1 -10
View File
@@ -26,7 +26,6 @@ export default function AdminSettingsTab({ admin, t }: AdminSettingsTabProps): R
placesEnrichEnabled, setPlacesEnrichEnabledState,
transitProvider, setTransitProviderState,
transitGoogleKeySource, setTransitGoogleKeySource,
trekPlacesEnabled, setTrekPlacesEnabledState,
placeShadowEnabled, setPlaceShadowEnabledState,
oidcConfig, setOidcConfig, savingOidc, setSavingOidc,
passwordLogin, setPasswordLogin, passwordRegistration, setPasswordRegistration,
@@ -382,15 +381,7 @@ export default function AdminSettingsTab({ admin, t }: AdminSettingsTabProps): R
{/* Three comparable blocks, in the order we would have people choose
them. Weather moved out entirely: it needs no key and had nothing
to configure, so it had no business in a card about keys. */}
<TrekApiCard
t={t}
enabled={trekPlacesEnabled}
onToggle={async () => {
const next = !trekPlacesEnabled
setTrekPlacesEnabledState(next)
try { await adminApi.updateTrekPlaces(next) } catch { setTrekPlacesEnabledState(!next) }
}}
/>
<TrekApiCard t={t} />
{!managed && (<>
<ProviderBlock title={t('admin.mapsKey')}>
+1 -24
View File
@@ -1,23 +1,10 @@
import React from 'react'
import { Check, ChevronRight, Globe2, KeyRound, Library, ShieldOff, WifiOff, X } from 'lucide-react'
import TrekMark from '../../components/shared/TrekMark'
import ToggleSwitch from '../../components/Settings/ToggleSwitch'
import type { TranslationFn } from '../../types'
interface TrekApiCardProps {
t: TranslationFn
/**
* Whether the index answers on this instance.
*
* A real switch rather than a badge, and the only one on this card that is
* about egress rather than about spending: with it on, every search and every
* keystroke in the place box leaves the instance for the service. An operator
* who does not want that has to be able to say so here — the flag was read by
* six call paths and written by nothing, so the only way to turn it off was an
* INSERT against app_settings by hand.
*/
enabled: boolean
onToggle: () => void
}
/**
@@ -39,7 +26,7 @@ interface TrekApiCardProps {
*/
const SOURCES = ['Overture Maps Foundation', 'OpenStreetMap', 'Wikivoyage', 'Wikimedia']
export default function TrekApiCard({ t, enabled, onToggle }: TrekApiCardProps): React.ReactElement {
export default function TrekApiCard({ t }: TrekApiCardProps): React.ReactElement {
// Reuses the words TREK already has for these fields wherever it has them,
// so the chip row costs two new strings instead of ten.
const fields = [
@@ -84,22 +71,12 @@ export default function TrekApiCard({ t, enabled, onToggle }: TrekApiCardProps):
<div className="px-5 pt-6 pb-4">
<div className="flex items-start justify-between gap-4">
<TrekMark className="h-8 w-auto text-content" aria-label="TREK Places API" />
<ToggleSwitch on={enabled} onToggle={onToggle} label={t('admin.trekApi.toggleLabel')} />
</div>
<p className="mt-3 text-sm leading-relaxed text-content-secondary">
{t('admin.trekApi.tagline')}
</p>
{/* Said only when it is off, and said plainly: this is the state where
searches stop using the index, and the reader should know what
answers instead rather than being left to guess. */}
{!enabled && (
<p className="mt-2 text-xs leading-relaxed text-content-faint">
{t('admin.trekApi.offNote')}
</p>
)}
<ul className="mt-4 grid grid-cols-1 gap-x-6 gap-y-2 sm:grid-cols-2">
{facts.map(({ Icon, text }) => (
<li key={text} className="flex items-center gap-2 text-xs text-content-secondary">
-3
View File
@@ -114,8 +114,6 @@ export function useAdmin() {
// false rather than the true the four switches above start from.
// The index switch. Read fail-open like the server does, so the state shown
// before the request lands matches what an unset row actually means.
const [trekPlacesEnabled, setTrekPlacesEnabledState] = useState<boolean>(true)
useEffect(() => { adminApi.getTrekPlaces().then(d => setTrekPlacesEnabledState(d.enabled)).catch(() => {}) }, [])
const [placeShadowEnabled, setPlaceShadowEnabledState] = useState<boolean>(false)
useEffect(() => { adminApi.getPlaceShadow().then(d => setPlaceShadowEnabledState(d.enabled)).catch(() => {}) }, [])
@@ -453,7 +451,6 @@ export function useAdmin() {
placesEnrichEnabled, setPlacesEnrichEnabledState,
transitProvider, setTransitProviderState,
transitGoogleKeySource, setTransitGoogleKeySource,
trekPlacesEnabled, setTrekPlacesEnabledState,
placeShadowEnabled, setPlaceShadowEnabledState,
collabFeatures, setCollabFeatures,
oidcConfig, setOidcConfig, savingOidc, setSavingOidc,
+12
View File
@@ -140,6 +140,18 @@ export function deriveMaps(raw: RawEnv) {
* who wants nothing to leave their network points at their own machine.
*/
trekPlacesUrl: raw.TREK_PLACES_URL || undefined,
/**
* Whether the index answers at all. On unless an operator says otherwise,
* because it is the path we want people on and an upgrade must not quietly
* drop back to Nominatim, whose usage policy forbids what TREK was doing
* with it.
*
* Deliberately an environment variable and not an admin switch: it decides
* whether searches leave the instance, and a setting that reaches for the
* network is one an operator wants pinned in their compose file, not one a
* second admin can flip in a browser.
*/
trekPlacesEnabled: raw.TREK_PLACES_ENABLED !== 'false',
placesApiKey: raw.PLACES_API_KEY || undefined,
/** Public pk.* token shipped with a managed instance; reaches the browser by design. */
mapboxToken: raw.MAPBOX_ACCESS_TOKEN || undefined,
+1
View File
@@ -144,6 +144,7 @@ export const envSchema = z.object({
TREK_MANAGED: boolStr,
PLACES_API_BASE: url,
TREK_PLACES_URL: url,
TREK_PLACES_ENABLED: boolStr,
PLACES_API_KEY: anyString,
MAPBOX_ACCESS_TOKEN: anyString,
CARTO_API_KEY: anyString,
-22
View File
@@ -218,28 +218,6 @@ export class AddonsService {
updatePlacesEnrich(enabled: boolean) { return this.writeFlag('places_enrich_enabled', enabled); }
/**
* The TREK place index, read fail-OPEN for the same reason enrichment is.
*
* This is the one switch on the card that is about egress rather than about
* spending: with it on, every search and every keystroke in the place box
* leaves the instance for places.liketrek.com. An operator who does not want
* that needs somewhere to say so, and until this pair existed the key was
* read by six call paths and written by nobody — the only way to turn the
* index off was an INSERT against app_settings by hand.
*
* It has to agree with MapsService.trekPlacesEnabled(), which reads the same
* key the same way. If the two ever disagree, the panel shows "off" while the
* queries keep leaving, which is the worst of both.
*/
getTrekPlaces() {
const row = this.db.prepare("SELECT value FROM app_settings WHERE key = 'trek_places_enabled'").get() as
| { value: string }
| undefined;
return { enabled: row?.value !== 'false' };
}
updateTrekPlaces(enabled: boolean) { return this.writeFlag('trek_places_enabled', enabled); }
// ── Transit backend (#1699) ────────────────────────────────────────────────
// Not a flag: two named backends, so it stores the name rather than a
// boolean. The read/write pair lives in transit/transit-provider.ts because
-14
View File
@@ -253,20 +253,6 @@ export class AdminController {
return result;
}
// The one switch here that is about egress rather than about spending: with
// the index on, every search and every keystroke in the place box leaves the
// instance. Six call paths read the key; until this pair existed nothing
// wrote it, so an operator who wanted it off had no way to say so.
@Get('trek-places')
getTrekPlaces() { return this.addons.getTrekPlaces(); }
@Put('trek-places')
updateTrekPlaces(@CurrentUser() user: User, @Body() body: AdminFeatureToggleDto, @Req() req: Request) {
const result = this.addons.updateTrekPlaces(body.enabled);
this.audit.writeAudit({ userId: user.id, action: 'admin.trek_places', ip: getClientIp(req), details: { enabled: result.enabled } });
return result;
}
@Get('collab-features')
getCollabFeatures() { return this.addons.getCollabFeatures(); }
+11 -8
View File
@@ -662,16 +662,19 @@ export class MapsService {
}
/**
* Fail-OPEN, unlike the three Google switches: the index is the path we want
* people on, and an install that upgrades without visiting the admin panel
* should land on it rather than stay on Nominatim, whose usage policy forbids
* what TREK was doing with it.
* Whether the index answers on this instance.
*
* On unless TREK_PLACES_ENABLED says otherwise, because the index is the path
* we want people on and an upgrade must not quietly drop back to Nominatim,
* whose usage policy forbids what TREK was doing with it.
*
* An environment variable rather than an admin switch on purpose. This decides
* whether a search leaves the instance at all, which is a property of the
* deployment: an operator pins it in their compose file, and it cannot be
* turned off from a browser by whoever holds an admin account that day.
*/
trekPlacesEnabled(): boolean {
const row = this.database.get<{ value: string }>(
"SELECT value FROM app_settings WHERE key = 'trek_places_enabled'",
);
return row?.value !== 'false';
return readEnv().maps.trekPlacesEnabled;
}
/**
@@ -7,6 +7,7 @@ import {
deriveHttp,
deriveSession,
deriveDemo,
deriveMaps,
deriveOidc,
deriveSmtp,
deriveMcp,
@@ -270,3 +271,16 @@ describe('deriveAll', () => {
}
});
});
describe('deriveMaps', () => {
it('TREK_PLACES_ENABLED is on unless it says the literal "false"', () => {
// Fail-open, and deliberately not the boolean-like family the switches
// above accept: a typo must not silently drop a whole install back to
// Nominatim, whose usage policy forbids what TREK was doing with it.
expect(deriveMaps({} as never).trekPlacesEnabled).toBe(true);
expect(deriveMaps({ TREK_PLACES_ENABLED: 'false' } as never).trekPlacesEnabled).toBe(false);
for (const value of ['true', 'FALSE', '0', 'no', '']) {
expect(deriveMaps({ TREK_PLACES_ENABLED: value } as never).trekPlacesEnabled, value).toBe(true);
}
});
});
@@ -495,91 +495,6 @@ describe('AddonsService transit provider', () => {
* PlaceShadowService.enabled(), which reads the same key itself — if the two
* diverge the admin panel shows "off" while the log keeps collecting picks.
*/
/**
* The index switch reads fail-OPEN, like places_enrich_enabled and unlike the
* three Google flags: nothing backfills a row on upgrade, so an absent row has
* to mean on, or every existing install would silently lose the index.
*
* It has to keep agreeing with MapsService.trekPlacesEnabled(), which reads the
* same key itself. If the two diverge, the admin panel shows "off" while every
* search and keystroke keeps leaving the instance.
*/
describe('AddonsService trek places flag', () => {
beforeEach(() => {
vi.clearAllMocks();
});
it('ADDONS-SVC-094 an unset flag reads as ON, and only the literal "false" turns it off', () => {
dbMock._stmt.get.mockReturnValueOnce(undefined);
expect(svc().getTrekPlaces()).toEqual({ enabled: true });
expect(dbMock.prepare).toHaveBeenLastCalledWith(
"SELECT value FROM app_settings WHERE key = 'trek_places_enabled'",
);
dbMock._stmt.get.mockReturnValueOnce({ value: 'false' });
expect(svc().getTrekPlaces()).toEqual({ enabled: false });
// Anything else is on. A garbled row must not silently disable the default
// search path for a whole install.
for (const value of ['true', 'FALSE', '0', '']) {
dbMock._stmt.get.mockReturnValueOnce({ value });
expect(svc().getTrekPlaces(), value).toEqual({ enabled: true });
}
});
it('ADDONS-SVC-095 the setter round-trips through the getter under its own key', () => {
// Keyed store rather than an echo assertion: the write has to produce the
// exact string the read compares against, so a setter persisting '0' fails
// here instead of silently reading back ON in production.
//
// The reader inlines its key in the SQL and binds nothing, the way the
// other fail-open flag does, so the statement text is what identifies the
// row here — not the bound parameter the writer uses.
const stored = new Map<string, string>();
let lastSql = '';
dbMock.prepare.mockImplementation((sql: string) => {
lastSql = sql;
return dbMock._stmt;
});
dbMock._stmt.run.mockImplementation((key: string, value: string) => {
stored.set(key, value);
});
dbMock._stmt.get.mockImplementation((key?: string) => {
const wanted = key ?? (lastSql.match(/key = '([^']+)'/)?.[1] ?? '');
const value = stored.get(wanted);
return value === undefined ? undefined : { value };
});
expect(svc().updateTrekPlaces(false)).toEqual({ enabled: false });
expect(dbMock._stmt.run).toHaveBeenLastCalledWith('trek_places_enabled', 'false');
expect(svc().getTrekPlaces()).toEqual({ enabled: false });
expect(svc().updateTrekPlaces(true)).toEqual({ enabled: true });
expect(dbMock._stmt.run).toHaveBeenLastCalledWith('trek_places_enabled', 'true');
expect(svc().getTrekPlaces()).toEqual({ enabled: true });
expect([...stored.keys()]).toEqual(['trek_places_enabled']);
});
it('ADDONS-SVC-096 answers the same as MapsService.trekPlacesEnabled() for every stored value', () => {
// The admin panel and the six call paths that gate on this must never
// disagree: showing "off" while queries keep leaving is worse than either
// default on its own.
const maps = new MapsService(new DatabaseService(dbConn), {} as never);
const rows: Array<[{ value: string } | undefined, boolean]> = [
[undefined, true],
[{ value: 'false' }, false],
[{ value: 'true' }, true],
[{ value: 'garbage' }, true],
];
for (const [row, expected] of rows) {
dbMock._stmt.get.mockReturnValueOnce(row).mockReturnValueOnce(row);
expect(svc().getTrekPlaces()).toEqual({ enabled: expected });
expect(maps.trekPlacesEnabled()).toBe(expected);
}
});
});
describe('AddonsService place shadow flag', () => {
beforeEach(() => {
vi.clearAllMocks();
@@ -49,8 +49,6 @@ const addonsStub = () => ({
getPlacesEnrich: vi.fn(() => ({ enabled: true })),
updatePlacesEnrich: vi.fn((enabled: boolean) => ({ enabled })),
// Fail-open too, and the only switch here about egress rather than spending.
getTrekPlaces: vi.fn(() => ({ enabled: true })),
updateTrekPlaces: vi.fn((enabled: boolean) => ({ enabled })),
getCollabFeatures: vi.fn(() => ({ chat: false })),
updateCollabFeatures: vi.fn(() => ({ features: { chat: true }, changed: true })),
getTransitProvider: vi.fn(() => ({ provider: 'transitous', googleKeySource: null })),
@@ -302,26 +300,6 @@ describe('AdminController feature toggles', () => {
);
});
it('ADMIN-TOGGLE-005 trek-places forwards the body value and audits the stored one', () => {
// The audit row carries what the service stored, not what the request
// asked for: this switch decides whether every search leaves the instance,
// so a row claiming it was turned off when it was not would be worse than
// no row at all.
const updateTrekPlaces = vi.fn(() => ({ enabled: true }));
const c = adminCtl(svc(), undefined, { ...addonsStub(), updateTrekPlaces } as unknown as AddonsService);
expect(c.updateTrekPlaces(user, { enabled: false }, req)).toEqual({ enabled: true });
expect(updateTrekPlaces).toHaveBeenCalledWith(false);
expect(writeAudit).toHaveBeenCalledWith({ userId: user.id, action: 'admin.trek_places', ip: '1.2.3.4', details: { enabled: true } });
});
it('ADMIN-TOGGLE-005b trek-places reads through to AddonsService instead of a controller-side default', () => {
const getTrekPlaces = vi.fn(() => ({ enabled: false }));
const c = adminCtl(svc(), undefined, { ...addonsStub(), getTrekPlaces } as unknown as AddonsService);
expect(c.getTrekPlaces()).toEqual({ enabled: false });
expect(getTrekPlaces).toHaveBeenCalledTimes(1);
expect(writeAudit).not.toHaveBeenCalled();
});
it('ADMIN-TOGGLE-004 place-shadow forwards the body value and audits the stored one', () => {
// The handler audits result.enabled. A service that declines the flip must
// not leave behind an audit row claiming the shadow log was switched on.
@@ -27,6 +27,22 @@ import { MapsService } from '../../../src/nest/maps/maps.service';
import type { DatabaseService } from '../../../src/nest/database/database.service';
import type { PlacePhotoCacheService } from '../../../src/nest/place-photos/place-photo-cache.service';
// The index switch is an environment variable now, not an admin row: it decides
// whether a search leaves the instance at all, so it is pinned by the operator
// rather than flippable from a browser. `setTrekPlaces()` below drives it.
const trekPlaces = vi.hoisted(() => ({ on: true }));
vi.mock('../../../src/app-config', async (importOriginal) => {
const actual = await importOriginal<typeof import('../../../src/app-config')>();
return {
...actual,
readEnv: () => {
const env = actual.readEnv();
return { ...env, maps: { ...env.maps, trekPlacesEnabled: trekPlaces.on } };
},
};
});
const INPUT = 'Café Kröpel';
const hit = (over: Record<string, unknown> = {}) => ({
@@ -47,11 +63,8 @@ const hit = (over: Record<string, unknown> = {}) => ({
* `'false'` to the key resolver and send the fallback at Google for real.
*/
function make(enabled = true) {
const database = {
get: vi.fn((sql: string) =>
sql.includes('trek_places_enabled') && !enabled ? { value: 'false' } : undefined,
),
} as unknown as DatabaseService;
trekPlaces.on = enabled;
const database = { get: vi.fn(() => undefined) } as unknown as DatabaseService;
return new MapsService(database, {} as PlacePhotoCacheService);
}
@@ -34,6 +34,22 @@ import { MapsService } from '../../../src/nest/maps/maps.service';
import type { DatabaseService } from '../../../src/nest/database/database.service';
import type { PlacePhotoCacheService } from '../../../src/nest/place-photos/place-photo-cache.service';
// The index switch is an environment variable now, not an admin row: it decides
// whether a search leaves the instance at all, so it is pinned by the operator
// rather than flippable from a browser. `setTrekPlaces()` below drives it.
const trekPlaces = vi.hoisted(() => ({ on: true }));
vi.mock('../../../src/app-config', async (importOriginal) => {
const actual = await importOriginal<typeof import('../../../src/app-config')>();
return {
...actual,
readEnv: () => {
const env = actual.readEnv();
return { ...env, maps: { ...env.maps, trekPlacesEnabled: trekPlaces.on } };
},
};
});
/** An index row, far enough from the OSM one below not to be deduped. */
const indexHit = (name: string, lat = 35.31, lng = 139.53) => ({
gers: name.toLowerCase().replace(/\W/g, ''),
@@ -62,11 +78,8 @@ const osmAnswer = (rows: unknown[]) => ({ ok: true, json: async () => rows });
* `'false'` to the API-key resolver, which would send the fallback at Google.
*/
function make(enabled = true) {
const database = {
get: vi.fn((sql: string) =>
sql.includes('trek_places_enabled') && !enabled ? { value: 'false' } : undefined,
),
} as unknown as DatabaseService;
trekPlaces.on = enabled;
const database = { get: vi.fn(() => undefined) } as unknown as DatabaseService;
return new MapsService(database, {} as PlacePhotoCacheService);
}
-4
View File
@@ -113,10 +113,6 @@ const admin: TranslationStrings = {
'admin.googleCaveat.badge': 'غير مُستحسَن',
'admin.googleCaveat.body':
'TREK مفتوح المصدر ولسنا محايدين هنا. بهذا الحجم لا توجد التقييمات وصور المحال العادية إلا لدى Google، وهذا هو الاحتكار. الحقل موجود لانعدام البديل، لا لأننا نوصي به. عندئذ يذهب كل استعلام إلى Google.',
'admin.trekApi.toggleLabel':
'فهرس أماكن TREK',
'admin.trekApi.offNote':
'مُعطَّل. يستخدم البحث عن الأماكن OpenStreetMap، ويستخدم Google عند ضبط مفتاح.',
'admin.trekApi.tagline':
'فهرس الأماكن الخاص بـ TREK. بحث بلا مفتاح من Google، بلا حصة، وبلا أن يُحصي أحد عمليات بحثك.',
'admin.trekApi.factPlaces':
-3
View File
@@ -87,7 +87,6 @@ const places: TranslationStrings = {
'places.loadingDetails': 'جارٍ تحميل تفاصيل المكان…',
'places.osmHint':
'يتم البحث عبر OpenStreetMap (بدون صور أو ساعات عمل أو تقييمات). أضف مفتاح Google API في الإعدادات للحصول على جميع التفاصيل.',
'places.osmActive': 'يتم استخدام OpenStreetMap. مفتاح Google API يضيف التقييمات وساعات العمل.',
'places.details.title': 'تفاصيل المكان',
'places.details.empty': 'اختر نتيجة بحث لعرض الصور والوصف.',
'places.details.loading': 'جارٍ تحميل التفاصيل...',
@@ -100,8 +99,6 @@ const places: TranslationStrings = {
'places.details.adoptBlocked': 'أفرغ حقل الوصف أولًا',
'places.details.facts': 'معلومات مفيدة',
'places.details.nothing': 'لم يتم العثور على شيء لهذا المكان.',
'places.details.noKeyTitle': 'تتوفر تفاصيل أكثر',
'places.details.noKeyHint': 'بدون مفتاح Google API يستخدم هذا التثبيت المصادر الحرة فقط. اطلب من مسؤول TREK إضافة مفتاح للحصول على التقييمات وساعات العمل والصور.',
'places.details.aboutBrand': 'عن السلسلة',
'places.details.aboutBrandNote': 'يصف السلسلة، لا هذا الفرع.',
'places.details.fact.rating': 'التقييم',
-2
View File
@@ -7,7 +7,6 @@ const roadtrip: TranslationStrings = {
'roadtrip.title': 'رحلة برية',
'roadtrip.subtitle': 'محطات التوقف وأوقات القيادة وأوقات الوصول لهذه الرحلة',
'roadtrip.empty.title': 'لا يوجد مسار بعد',
'roadtrip.empty.body': 'أضف أماكن إلى يوم أو حجز سيارة مع محطات توقف، وسيظهر المسار هنا.',
'roadtrip.stops.label': 'محطات على الطريق',
'roadtrip.stops.remove': 'إزالة المحطة',
'roadtrip.poi.title': 'على طول المسار',
@@ -31,7 +30,6 @@ const roadtrip: TranslationStrings = {
'roadtrip.limit.range': '{distance} منذ آخر تعبئة',
'roadtrip.limit.none': 'غير محددة',
'roadtrip.stop.none': 'وجهة',
'roadtrip.stop.kindHelp': 'التوقف في الطريق يُرسم على المسار ولا يُحتسب ضمن عدد التوقفات.',
'roadtrip.stay.mode.pause': 'استراحة',
'roadtrip.stay.mode.night': 'مبيت',
'roadtrip.stay.checkOutDay': 'المغادرة في',
-4
View File
@@ -91,10 +91,6 @@ const admin: TranslationStrings = {
'admin.googleCaveat.badge': 'Não recomendado',
'admin.googleCaveat.body':
'O TREK é código aberto e aqui não somos neutros. Nesta escala, avaliações e fotos de estabelecimentos comuns só existem no Google, e isso é um monopólio. O campo está aqui por falta de alternativa, não porque o recomendamos. Aí cada consulta vai para o Google.',
'admin.trekApi.toggleLabel':
'Índice de lugares do TREK',
'admin.trekApi.offNote':
'Desativado. A busca de lugares usa o OpenStreetMap e o Google quando há uma chave configurada.',
'admin.trekApi.tagline':
'O índice de locais do próprio TREK. Pesquisar sem chave do Google, sem cota e sem que ninguém conte as suas buscas.',
'admin.trekApi.factPlaces':
-3
View File
@@ -87,7 +87,6 @@ const places: TranslationStrings = {
'places.loadingDetails': 'Carregando detalhes do lugar…',
'places.osmHint':
'Busca via OpenStreetMap (sem fotos, horários ou avaliações). Adicione uma chave Google nas configurações para detalhes completos.',
'places.osmActive': 'Usando o OpenStreetMap. Uma chave de API do Google adiciona avaliações e horários.',
'places.details.title': 'Detalhes do lugar',
'places.details.empty': 'Escolha um resultado para ver imagens e uma descrição.',
'places.details.loading': 'Carregando detalhes...',
@@ -100,8 +99,6 @@ const places: TranslationStrings = {
'places.details.adoptBlocked': 'Esvazie primeiro o campo de descrição',
'places.details.facts': 'Bom saber',
'places.details.nothing': 'Nada encontrado para este lugar.',
'places.details.noKeyTitle': 'Mais detalhes disponíveis',
'places.details.noKeyHint': 'Sem uma chave de API do Google, esta instância usa apenas fontes livres. Peça ao administrador do TREK para adicionar uma para avaliações, horários e fotos.',
'places.details.aboutBrand': 'Sobre a rede',
'places.details.aboutBrandNote': 'Descreve a rede, não esta unidade.',
'places.details.fact.rating': 'Avaliação',
-2
View File
@@ -7,7 +7,6 @@ const roadtrip: TranslationStrings = {
'roadtrip.title': 'Viagem de carro',
'roadtrip.subtitle': 'Paradas, tempos de direção e horários de chegada desta viagem',
'roadtrip.empty.title': 'Ainda sem rota',
'roadtrip.empty.body': 'Adicione locais a um dia ou uma reserva de carro com paradas e a rota aparecerá aqui.',
'roadtrip.stops.label': 'Paradas pelo caminho',
'roadtrip.stops.remove': 'Remover parada',
'roadtrip.poi.title': 'Ao longo da rota',
@@ -31,7 +30,6 @@ const roadtrip: TranslationStrings = {
'roadtrip.limit.range': '{distance} desde o último abastecimento',
'roadtrip.limit.none': 'Nenhum definido',
'roadtrip.stop.none': 'Um destino',
'roadtrip.stop.kindHelp': 'Uma parada no caminho é desenhada no trajeto e não é contada.',
'roadtrip.stay.mode.pause': 'Pausa',
'roadtrip.stay.mode.night': 'Pernoite',
'roadtrip.stay.checkOutDay': 'Saída em',
-4
View File
@@ -98,10 +98,6 @@ const admin: TranslationStrings = {
'admin.googleCaveat.badge': 'No recomanat',
'admin.googleCaveat.body':
"TREK és programari lliure i aquí no som neutrals. En aquesta escala, les valoracions i les fotos de negocis corrents només existeixen a Google, i això és un monopoli. El camp hi és per manca d'alternativa, no perquè el recomanem. Llavors cada consulta va a Google.",
'admin.trekApi.toggleLabel':
'Índex de llocs de TREK',
'admin.trekApi.offNote':
'Desactivat. La cerca de llocs utilitza OpenStreetMap i Google si hi ha una clau configurada.',
'admin.trekApi.tagline':
'L\'índex de llocs propi de TREK. Cercar sense clau de Google, sense quota i sense que ningú compti les teves cerques.',
'admin.trekApi.factPlaces':
-3
View File
@@ -85,7 +85,6 @@ const places: TranslationStrings = {
'places.loadingDetails': 'Carregant els detalls del lloc…',
'places.osmHint':
"S'està utilitzant la cerca amb OpenStreetMap (sense fotos, horaris ni valoracions). Afegeix una clau API de Google a Configuració per obtenir tots els detalls.",
'places.osmActive': 'S\'utilitza OpenStreetMap. Una clau API de Google afegeix valoracions i horaris.',
'places.details.title': 'Detalls del lloc',
'places.details.empty': 'Tria un resultat per veure imatges i una descripció.',
'places.details.loading': "S'estan carregant els detalls...",
@@ -98,8 +97,6 @@ const places: TranslationStrings = {
'places.details.adoptBlocked': 'Buida primer el camp de descripció',
'places.details.facts': 'Bo saber',
'places.details.nothing': "No s'ha trobat res per a aquest lloc.",
'places.details.noKeyTitle': 'Hi ha més detalls disponibles',
'places.details.noKeyHint': 'Sense una clau API de Google, aquesta instància només fa servir fonts lliures. Demana a l\'administració de TREK que n\'afegeixi una per a valoracions, horaris i fotos.',
'places.details.aboutBrand': 'Sobre la cadena',
'places.details.aboutBrandNote': 'Descriu la cadena, no aquest local.',
'places.details.fact.rating': 'Valoració',
-2
View File
@@ -7,7 +7,6 @@ const roadtrip: TranslationStrings = {
'roadtrip.title': 'Viatge per carretera',
'roadtrip.subtitle': 'Parades, temps de conducció i hores d’arribada d’aquest viatge',
'roadtrip.empty.title': 'Encara no hi ha ruta',
'roadtrip.empty.body': 'Afegeix llocs a un dia o una reserva de cotxe amb parades i la ruta apareixerà aquí.',
'roadtrip.stops.label': 'Parades pel camí',
'roadtrip.stops.remove': 'Elimina la parada',
'roadtrip.poi.title': 'Al llarg de la ruta',
@@ -31,7 +30,6 @@ const roadtrip: TranslationStrings = {
'roadtrip.limit.range': '{distance} des de l’última càrrega',
'roadtrip.limit.none': 'Cap definit',
'roadtrip.stop.none': 'Una destinació',
'roadtrip.stop.kindHelp': 'Una parada al camí es dibuixa sobre el trajecte i no es compta.',
'roadtrip.stay.mode.pause': 'Parada',
'roadtrip.stay.mode.night': 'Pernoctar',
'roadtrip.stay.checkOutDay': 'Sortida el',
-4
View File
@@ -91,10 +91,6 @@ const admin: TranslationStrings = {
'admin.googleCaveat.badge': 'Nedoporučeno',
'admin.googleCaveat.body':
'TREK je otevřený software a nejsme v tom neutrální. V tomto měřítku existují hodnocení a fotografie běžných podniků jen u Googlu, a to je monopol. Pole tu je z nedostatku alternativy, ne proto, že bychom je doporučovali. Každý dotaz pak jde do Googlu.',
'admin.trekApi.toggleLabel':
'Index míst TREK',
'admin.trekApi.offNote':
'Vypnuto. Vyhledávání míst používá OpenStreetMap a Google, pokud je nastaven klíč.',
'admin.trekApi.tagline':
'Vlastní rejstřík míst TREKu. Hledání bez klíče od Googlu, bez kvóty a bez toho, aby někdo počítal vaše dotazy.',
'admin.trekApi.factPlaces':
-3
View File
@@ -87,7 +87,6 @@ const places: TranslationStrings = {
'places.loadingDetails': 'Načítání podrobností místa…',
'places.osmHint':
'Používáte hledání přes OpenStreetMap (bez fotek a hodnocení). Pro plné detaily přidejte Google API klíč v nastavení.',
'places.osmActive': 'Používá se OpenStreetMap. Klíč Google API přidá hodnocení a otevírací dobu.',
'places.details.title': 'Podrobnosti místa',
'places.details.empty': 'Vyber výsledek hledání a zobrazí se obrázky a popis.',
'places.details.loading': 'Načítání podrobností...',
@@ -100,8 +99,6 @@ const places: TranslationStrings = {
'places.details.adoptBlocked': 'Nejdřív vyprázdni pole popisu',
'places.details.facts': 'Dobré vědět',
'places.details.nothing': 'Pro toto místo se nic nenašlo.',
'places.details.noKeyTitle': 'K dispozici je více podrobností',
'places.details.noKeyHint': 'Bez klíče Google API používá tato instance jen volné zdroje. Požádejte správce TREK o jeho přidání pro hodnocení, otevírací dobu a fotografie.',
'places.details.aboutBrand': 'O řetězci',
'places.details.aboutBrandNote': 'Popisuje řetězec, ne tuto pobočku.',
'places.details.fact.rating': 'Hodnocení',
-2
View File
@@ -7,7 +7,6 @@ const roadtrip: TranslationStrings = {
'roadtrip.title': 'Cesta autem',
'roadtrip.subtitle': 'Zastávky, doby jízdy a časy příjezdu této cesty',
'roadtrip.empty.title': 'Zatím žádná trasa',
'roadtrip.empty.body': 'Přidejte místa ke dni nebo rezervaci auta se zastávkami a trasa se zobrazí zde.',
'roadtrip.stops.label': 'Zastávky po cestě',
'roadtrip.stops.remove': 'Odebrat zastávku',
'roadtrip.poi.title': 'Podél trasy',
@@ -31,7 +30,6 @@ const roadtrip: TranslationStrings = {
'roadtrip.limit.range': '{distance} od posledního tankování',
'roadtrip.limit.none': 'Nenastaveno',
'roadtrip.stop.none': 'Cíl cesty',
'roadtrip.stop.kindHelp': 'Zastávka po cestě se kreslí na trase a nepočítá se.',
'roadtrip.stay.mode.pause': 'Pauza',
'roadtrip.stay.mode.night': 'Nocleh',
'roadtrip.stay.checkOutDay': 'Odjezd',
-4
View File
@@ -93,10 +93,6 @@ const admin: TranslationStrings = {
'admin.googleCaveat.badge': 'Nicht empfohlen',
'admin.googleCaveat.body':
'TREK ist quelloffen und wir sind hier nicht neutral. Bewertungen und Fotos gewöhnlicher Läden gibt es in dieser Größenordnung nur bei Google, und genau das ist ein Monopol. Das Feld steht hier, weil es keine Alternative gibt, nicht weil wir es empfehlen. Jede Abfrage geht dann an Google.',
'admin.trekApi.toggleLabel':
'TREK-Ortsindex',
'admin.trekApi.offNote':
'Ausgeschaltet. Die Ortssuche nutzt OpenStreetMap und, sofern ein Schlüssel hinterlegt ist, Google.',
'admin.trekApi.tagline':
'TREKs eigener Ortsindex. Suchen ohne Google-Schlüssel, ohne Kontingent und ohne dass jemand mitzählt.',
'admin.trekApi.factPlaces':
-3
View File
@@ -87,7 +87,6 @@ const places: TranslationStrings = {
'places.loadingDetails': 'Ortsdetails werden geladen…',
'places.osmHint':
'OpenStreetMap-Suche aktiv (ohne Bilder, Öffnungszeiten, Bewertungen). Für erweiterte Daten Google API Key in den Einstellungen hinterlegen.',
'places.osmActive': 'OpenStreetMap wird genutzt. Ein Google-API-Key ergänzt Bewertungen und Öffnungszeiten.',
'places.details.title': 'Ortsdetails',
'places.details.empty': 'Wähle ein Suchergebnis, um Bilder und eine Beschreibung zu sehen.',
'places.details.loading': 'Details werden geladen...',
@@ -100,8 +99,6 @@ const places: TranslationStrings = {
'places.details.adoptBlocked': 'Erst das Beschreibungsfeld leeren',
'places.details.facts': 'Gut zu wissen',
'places.details.nothing': 'Zu diesem Ort wurde nichts gefunden.',
'places.details.noKeyTitle': 'Mehr Details möglich',
'places.details.noKeyHint': 'Ohne Google-API-Key nutzt diese Instanz nur freie Quellen. Wende dich an deine TREK-Administration, um Bewertungen, Öffnungszeiten und Fotos freizuschalten.',
'places.details.aboutBrand': 'Über die Kette',
'places.details.aboutBrandNote': 'Beschreibt die Kette, nicht diese Filiale.',
'places.details.fact.rating': 'Bewertung',
-2
View File
@@ -7,7 +7,6 @@ const roadtrip: TranslationStrings = {
'roadtrip.title': 'Roadtrip',
'roadtrip.subtitle': 'Stopps, Fahrzeiten und Ankunftszeiten dieser Reise',
'roadtrip.empty.title': 'Noch keine Route',
'roadtrip.empty.body': 'Ordne einem Tag Orte zu oder lege eine Autofahrt mit Stopps an — die Route erscheint dann hier.',
'roadtrip.stops.label': 'Stopps unterwegs',
'roadtrip.stops.remove': 'Stopp entfernen',
'roadtrip.poi.title': 'Entlang der Route',
@@ -31,7 +30,6 @@ const roadtrip: TranslationStrings = {
'roadtrip.limit.range': '{distance} seit dem letzten Tanken',
'roadtrip.limit.none': 'Keine gesetzt',
'roadtrip.stop.none': 'Ein Ziel',
'roadtrip.stop.kindHelp': 'Ein Halt unterwegs wird auf der Fahrt gezeichnet und nicht mitgezählt.',
'roadtrip.stay.mode.pause': 'Pause',
'roadtrip.stay.mode.night': 'Übernachten',
'roadtrip.stay.checkOutDay': 'Abreise am',
-4
View File
@@ -133,10 +133,6 @@ const admin: TranslationStrings = {
'admin.googleCaveat.badge': 'Not recommended',
'admin.googleCaveat.body':
'TREK is open source and we are not neutral here. Ratings and photos of ordinary businesses exist at this scale only at Google, and that is what a monopoly is. The field is here because there is no alternative, not because we recommend it. Every lookup then goes to Google.',
'admin.trekApi.toggleLabel':
'TREK place index',
'admin.trekApi.offNote':
'Switched off. Place search uses OpenStreetMap, and Google where a key is set.',
'admin.trekApi.tagline':
'TREK\'s own place index. Search without a Google key, without a quota and without anyone counting your lookups.',
'admin.trekApi.factPlaces':
-3
View File
@@ -87,7 +87,6 @@ const places: TranslationStrings = {
'places.loadingDetails': 'Loading place details…',
'places.osmHint':
'Using OpenStreetMap search (no photos, opening hours, or ratings). Add a Google API key in settings for full details.',
'places.osmActive': 'Using OpenStreetMap. A Google API key adds ratings and opening hours.',
'places.details.title': 'Place details',
'places.details.empty': 'Pick a search result to see pictures and a description.',
'places.details.loading': 'Loading details...',
@@ -100,8 +99,6 @@ const places: TranslationStrings = {
'places.details.adoptBlocked': 'Clear the description field first',
'places.details.facts': 'Good to know',
'places.details.nothing': 'Nothing found for this place.',
'places.details.noKeyTitle': 'More detail is available',
'places.details.noKeyHint': 'Without a Google API key this instance can only use free sources. Ask your TREK administrator to add one for ratings, opening hours and photos.',
'places.details.aboutBrand': 'About the chain',
'places.details.aboutBrandNote': 'This describes the chain, not this branch.',
'places.details.fact.rating': 'Rating',
-2
View File
@@ -7,7 +7,6 @@ const roadtrip: TranslationStrings = {
'roadtrip.title': 'Road trip',
'roadtrip.subtitle': 'Stops, driving times and arrival times for this trip',
'roadtrip.empty.title': 'No route yet',
'roadtrip.empty.body': 'Add places to a day, or a car booking with stops, and the route appears here.',
'roadtrip.stops.label': 'Stops along the way',
'roadtrip.stops.remove': 'Remove stop',
'roadtrip.poi.title': 'Along the route',
@@ -31,7 +30,6 @@ const roadtrip: TranslationStrings = {
'roadtrip.limit.range': '{distance} since the last fill-up',
'roadtrip.limit.none': 'None set',
'roadtrip.stop.none': 'A destination',
'roadtrip.stop.kindHelp': 'A pause on the way is drawn on the drive and left out of the stop count.',
'roadtrip.stay.mode.pause': 'Pause',
'roadtrip.stay.mode.night': 'Overnight',
'roadtrip.stay.checkOutDay': 'Check out on',
-4
View File
@@ -93,10 +93,6 @@ const admin: TranslationStrings = {
'admin.googleCaveat.badge': 'No recomendado',
'admin.googleCaveat.body':
'TREK es software libre y aquí no somos neutrales. A esta escala, las valoraciones y las fotos de negocios corrientes solo existen en Google, y eso es un monopolio. El campo está aquí por falta de alternativa, no porque lo recomendemos. Cada consulta va entonces a Google.',
'admin.trekApi.toggleLabel':
'Índice de lugares TREK',
'admin.trekApi.offNote':
'Desactivado. La búsqueda de lugares usa OpenStreetMap y Google si hay una clave configurada.',
'admin.trekApi.tagline':
'El índice de lugares propio de TREK. Buscar sin clave de Google, sin cuota y sin que nadie cuente tus búsquedas.',
'admin.trekApi.factPlaces':
-3
View File
@@ -87,7 +87,6 @@ const places: TranslationStrings = {
'places.loadingDetails': 'Cargando detalles del lugar…',
'places.osmHint':
'Usando búsqueda con OpenStreetMap (sin fotos, horarios ni valoraciones). Añade una clave API de Google en Ajustes para obtener todos los detalles.',
'places.osmActive': 'Se usa OpenStreetMap. Una clave de API de Google añade valoraciones y horarios.',
'places.details.title': 'Detalles del lugar',
'places.details.empty': 'Elige un resultado para ver imágenes y una descripción.',
'places.details.loading': 'Cargando detalles...',
@@ -100,8 +99,6 @@ const places: TranslationStrings = {
'places.details.adoptBlocked': 'Vacía primero el campo de descripción',
'places.details.facts': 'Bueno saber',
'places.details.nothing': 'No se encontró nada para este lugar.',
'places.details.noKeyTitle': 'Hay más detalles disponibles',
'places.details.noKeyHint': 'Sin una clave de API de Google, esta instancia solo usa fuentes libres. Pide a tu administrador de TREK que añada una para valoraciones, horarios y fotos.',
'places.details.aboutBrand': 'Sobre la cadena',
'places.details.aboutBrandNote': 'Describe la cadena, no este local.',
'places.details.fact.rating': 'Valoración',
-2
View File
@@ -7,7 +7,6 @@ const roadtrip: TranslationStrings = {
'roadtrip.title': 'Viaje por carretera',
'roadtrip.subtitle': 'Paradas, tiempos de conducción y horas de llegada de este viaje',
'roadtrip.empty.title': 'Aún no hay ruta',
'roadtrip.empty.body': 'Añade lugares a un día o una reserva de coche con paradas y la ruta aparecerá aquí.',
'roadtrip.stops.label': 'Paradas por el camino',
'roadtrip.stops.remove': 'Eliminar parada',
'roadtrip.poi.title': 'A lo largo de la ruta',
@@ -31,7 +30,6 @@ const roadtrip: TranslationStrings = {
'roadtrip.limit.range': '{distance} desde el último repostaje',
'roadtrip.limit.none': 'Ninguno definido',
'roadtrip.stop.none': 'Un destino',
'roadtrip.stop.kindHelp': 'Una parada en el camino se dibuja sobre el trayecto y no se cuenta.',
'roadtrip.stay.mode.pause': 'Parada',
'roadtrip.stay.mode.night': 'Pernoctar',
'roadtrip.stay.checkOutDay': 'Salida el',
-4
View File
@@ -92,10 +92,6 @@ const admin: TranslationStrings = {
'admin.googleCaveat.badge': 'Déconseillé',
'admin.googleCaveat.body':
"TREK est un logiciel libre et nous ne sommes pas neutres ici. À cette échelle, les avis et les photos de commerces ordinaires n'existent que chez Google, et c'est cela, un monopole. Le champ est là faute d'alternative, pas parce que nous le recommandons. Chaque requête part alors chez Google.",
'admin.trekApi.toggleLabel':
'Index de lieux TREK',
'admin.trekApi.offNote':
'Désactivé. La recherche de lieux utilise OpenStreetMap, et Google si une clé est configurée.',
'admin.trekApi.tagline':
'L\'index de lieux propre à TREK. Chercher sans clé Google, sans quota et sans que personne ne compte vos recherches.',
'admin.trekApi.factPlaces':
-3
View File
@@ -87,7 +87,6 @@ const places: TranslationStrings = {
'places.loadingDetails': 'Chargement des détails du lieu…',
'places.osmHint':
'Recherche via OpenStreetMap (pas de photos, horaires ni notes). Ajoutez une clé API Google dans les paramètres pour plus de détails.',
'places.osmActive': 'OpenStreetMap est utilisé. Une clé API Google ajoute notes et horaires.',
'places.details.title': 'Détails du lieu',
'places.details.empty': 'Choisis un résultat pour voir des images et une description.',
'places.details.loading': 'Chargement des détails...',
@@ -100,8 +99,6 @@ const places: TranslationStrings = {
'places.details.adoptBlocked': "Videz d'abord le champ description",
'places.details.facts': 'Bon à savoir',
'places.details.nothing': 'Rien trouvé pour ce lieu.',
'places.details.noKeyTitle': 'Plus de détails disponibles',
'places.details.noKeyHint': 'Sans clé API Google, cette instance n\'utilise que des sources libres. Demandez à votre administrateur TREK d\'en ajouter une pour les avis, les horaires et les photos.',
'places.details.aboutBrand': 'À propos de la chaîne',
'places.details.aboutBrandNote': 'Décrit la chaîne, pas cet établissement.',
'places.details.fact.rating': 'Note',
-2
View File
@@ -7,7 +7,6 @@ const roadtrip: TranslationStrings = {
'roadtrip.title': 'Road trip',
'roadtrip.subtitle': 'Étapes, temps de conduite et heures d’arrivée de ce voyage',
'roadtrip.empty.title': 'Pas encore d’itinéraire',
'roadtrip.empty.body': 'Ajoutez des lieux à une journée ou une location de voiture avec des étapes : l’itinéraire apparaîtra ici.',
'roadtrip.stops.label': 'Étapes en chemin',
'roadtrip.stops.remove': 'Supprimer l’étape',
'roadtrip.poi.title': 'Le long de l’itinéraire',
@@ -31,7 +30,6 @@ const roadtrip: TranslationStrings = {
'roadtrip.limit.range': '{distance} depuis le dernier plein',
'roadtrip.limit.none': 'Aucune définie',
'roadtrip.stop.none': 'Une destination',
'roadtrip.stop.kindHelp': 'Un arrêt en chemin est dessiné sur le trajet et n’est pas compté.',
'roadtrip.stay.mode.pause': 'Pause',
'roadtrip.stay.mode.night': 'Nuitée',
'roadtrip.stay.checkOutDay': 'Départ le',
-4
View File
@@ -138,10 +138,6 @@ const admin: TranslationStrings = {
'admin.googleCaveat.badge': 'Δεν συνιστάται',
'admin.googleCaveat.body':
'Το TREK είναι ανοιχτού κώδικα και εδώ δεν είμαστε ουδέτεροι. Σε αυτή την κλίμακα, βαθμολογίες και φωτογραφίες συνηθισμένων καταστημάτων υπάρχουν μόνο στη Google, και αυτό είναι μονοπώλιο. Το πεδίο υπάρχει ελλείψει εναλλακτικής, όχι επειδή το συνιστούμε. Τότε κάθε αναζήτηση πηγαίνει στη Google.',
'admin.trekApi.toggleLabel':
'Ευρετήριο τοποθεσιών TREK',
'admin.trekApi.offNote':
'Απενεργοποιημένο. Η αναζήτηση τοποθεσιών χρησιμοποιεί το OpenStreetMap και το Google όπου έχει οριστεί κλειδί.',
'admin.trekApi.tagline':
'Το δικό του ευρετήριο τόπων του TREK. Αναζήτηση χωρίς κλειδί Google, χωρίς όριο και χωρίς να μετράει κανείς τις αναζητήσεις σας.',
'admin.trekApi.factPlaces':
-3
View File
@@ -87,7 +87,6 @@ const places: TranslationStrings = {
'places.loadingDetails': 'Φόρτωση λεπτομερειών μέρους…',
'places.osmHint':
'Χρήση αναζήτησης OpenStreetMap (χωρίς φωτογραφίες, ώρες λειτουργίας ή αξιολογήσεις). Προσθέστε ένα κλειδί Google API στις ρυθμίσεις για πλήρεις λεπτομέρειες.',
'places.osmActive': 'Χρησιμοποιείται το OpenStreetMap. Ένα κλειδί Google API προσθέτει αξιολογήσεις και ωράρια.',
'places.details.title': 'Λεπτομέρειες τοποθεσίας',
'places.details.empty': 'Διάλεξε ένα αποτέλεσμα για να δεις εικόνες και περιγραφή.',
'places.details.loading': 'Φόρτωση λεπτομερειών...',
@@ -100,8 +99,6 @@ const places: TranslationStrings = {
'places.details.adoptBlocked': 'Άδειασε πρώτα το πεδίο περιγραφής',
'places.details.facts': 'Καλό να ξέρεις',
'places.details.nothing': 'Δεν βρέθηκε τίποτα για αυτήν την τοποθεσία.',
'places.details.noKeyTitle': 'Διατίθενται περισσότερες λεπτομέρειες',
'places.details.noKeyHint': 'Χωρίς κλειδί Google API, αυτή η εγκατάσταση χρησιμοποιεί μόνο ελεύθερες πηγές. Ζητήστε από τον διαχειριστή TREK να προσθέσει ένα για βαθμολογίες, ώρες λειτουργίας και φωτογραφίες.',
'places.details.aboutBrand': 'Σχετικά με την αλυσίδα',
'places.details.aboutBrandNote': 'Περιγράφει την αλυσίδα, όχι αυτό το κατάστημα.',
'places.details.fact.rating': 'Βαθμολογία',
-2
View File
@@ -7,7 +7,6 @@ const roadtrip: TranslationStrings = {
'roadtrip.title': 'Οδικό ταξίδι',
'roadtrip.subtitle': 'Στάσεις, χρόνοι οδήγησης και ώρες άφιξης αυτού του ταξιδιού',
'roadtrip.empty.title': 'Δεν υπάρχει διαδρομή ακόμη',
'roadtrip.empty.body': 'Προσθέστε τοποθεσίες σε μια ημέρα ή μια κράτηση αυτοκινήτου με στάσεις και η διαδρομή θα εμφανιστεί εδώ.',
'roadtrip.stops.label': 'Στάσεις στη διαδρομή',
'roadtrip.stops.remove': 'Αφαίρεση στάσης',
'roadtrip.poi.title': 'Κατά μήκος της διαδρομής',
@@ -31,7 +30,6 @@ const roadtrip: TranslationStrings = {
'roadtrip.limit.range': '{distance} από τον τελευταίο ανεφοδιασμό',
'roadtrip.limit.none': 'Δεν έχουν οριστεί',
'roadtrip.stop.none': 'Προορισμός',
'roadtrip.stop.kindHelp': 'Μια στάση στον δρόμο σχεδιάζεται πάνω στη διαδρομή και δεν προσμετράται.',
'roadtrip.stay.mode.pause': 'Στάση',
'roadtrip.stay.mode.night': 'Διανυκτέρευση',
'roadtrip.stay.checkOutDay': 'Αναχώρηση',
-4
View File
@@ -92,10 +92,6 @@ const admin: TranslationStrings = {
'admin.googleCaveat.badge': 'Nem ajánlott',
'admin.googleCaveat.body':
'A TREK nyílt forráskódú, és ebben nem vagyunk semlegesek. Ebben a méretben az értékelések és a hétköznapi üzletek fotói csak a Google-nél léteznek, és ez a monopólium. A mező alternatíva híján van itt, nem mert ajánljuk. Minden lekérdezés ilyenkor a Google-höz megy.',
'admin.trekApi.toggleLabel':
'TREK helyindex',
'admin.trekApi.offNote':
'Kikapcsolva. A helykeresés az OpenStreetMapot használja, és a Google-t, ha van beállított kulcs.',
'admin.trekApi.tagline':
'A TREK saját helyindexe. Keresés Google-kulcs nélkül, kvóta nélkül, és anélkül, hogy bárki számolná a lekérdezéseidet.',
'admin.trekApi.factPlaces':
-3
View File
@@ -87,7 +87,6 @@ const places: TranslationStrings = {
'places.loadingDetails': 'Hely adatainak betöltése…',
'places.osmHint':
'OpenStreetMap keresés aktív (képek, nyitvatartás és értékelések nélkül). Bővített adatokhoz add meg a Google API kulcsot a beállításokban.',
'places.osmActive': 'Az OpenStreetMap van használatban. Egy Google API-kulcs értékeléseket és nyitvatartást ad hozzá.',
'places.details.title': 'Hely részletei',
'places.details.empty': 'Válassz egy találatot a képekhez és a leíráshoz.',
'places.details.loading': 'Részletek betöltése...',
@@ -100,8 +99,6 @@ const places: TranslationStrings = {
'places.details.adoptBlocked': 'Előbb ürítsd ki a leírás mezőt',
'places.details.facts': 'Jó tudni',
'places.details.nothing': 'Ehhez a helyhez nem található semmi.',
'places.details.noKeyTitle': 'További részletek érhetők el',
'places.details.noKeyHint': 'Google API-kulcs nélkül ez a példány csak szabad forrásokat használ. Kérd a TREK rendszergazdáját, hogy adjon hozzá egyet az értékelésekhez, nyitvatartáshoz és fotókhoz.',
'places.details.aboutBrand': 'A láncról',
'places.details.aboutBrandNote': 'A láncot írja le, nem ezt az egységet.',
'places.details.fact.rating': 'Értékelés',
-2
View File
@@ -7,7 +7,6 @@ const roadtrip: TranslationStrings = {
'roadtrip.title': 'Autós út',
'roadtrip.subtitle': 'Az utazás megállói, vezetési idői és érkezési időpontjai',
'roadtrip.empty.title': 'Még nincs útvonal',
'roadtrip.empty.body': 'Adj helyszíneket egy naphoz, vagy hozz létre megállókkal rendelkező autós foglalást – az útvonal itt jelenik meg.',
'roadtrip.stops.label': 'Megállók útközben',
'roadtrip.stops.remove': 'Megálló eltávolítása',
'roadtrip.poi.title': 'Az útvonal mentén',
@@ -31,7 +30,6 @@ const roadtrip: TranslationStrings = {
'roadtrip.limit.range': '{distance} az utolsó tankolás óta',
'roadtrip.limit.none': 'Nincs beállítva',
'roadtrip.stop.none': 'Úti cél',
'roadtrip.stop.kindHelp': 'Az útközbeni megálló az útvonalra kerül, és nem számít bele.',
'roadtrip.stay.mode.pause': 'Szünet',
'roadtrip.stay.mode.night': 'Éjszakázás',
'roadtrip.stay.checkOutDay': 'Kijelentkezés',
-4
View File
@@ -134,10 +134,6 @@ const admin: TranslationStrings = {
'admin.googleCaveat.badge': 'Tidak disarankan',
'admin.googleCaveat.body':
'TREK adalah perangkat lunak sumber terbuka dan di sini kami tidak netral. Pada skala ini ulasan dan foto usaha biasa hanya ada di Google, dan itulah monopoli. Kolom ini ada karena tidak ada penggantinya, bukan karena kami menyarankan. Lalu setiap pencarian pergi ke Google.',
'admin.trekApi.toggleLabel':
'Indeks tempat TREK',
'admin.trekApi.offNote':
'Dimatikan. Pencarian tempat memakai OpenStreetMap, dan Google bila ada kunci yang disetel.',
'admin.trekApi.tagline':
'Indeks tempat milik TREK sendiri. Mencari tanpa kunci Google, tanpa kuota, dan tanpa ada yang menghitung pencarian Anda.',
'admin.trekApi.factPlaces':
-3
View File
@@ -86,7 +86,6 @@ const places: TranslationStrings = {
'places.loadingDetails': 'Memuat detail tempat…',
'places.osmHint':
'Menggunakan pencarian OpenStreetMap (tanpa foto, jam buka, atau penilaian). Tambahkan Google API key di pengaturan untuk detail lengkap.',
'places.osmActive': 'Menggunakan OpenStreetMap. Kunci API Google menambahkan rating dan jam buka.',
'places.details.title': 'Detail tempat',
'places.details.empty': 'Pilih hasil pencarian untuk melihat gambar dan deskripsi.',
'places.details.loading': 'Memuat detail...',
@@ -99,8 +98,6 @@ const places: TranslationStrings = {
'places.details.adoptBlocked': 'Kosongkan dulu kolom deskripsi',
'places.details.facts': 'Perlu diketahui',
'places.details.nothing': 'Tidak ada yang ditemukan untuk tempat ini.',
'places.details.noKeyTitle': 'Tersedia detail lebih lanjut',
'places.details.noKeyHint': 'Tanpa kunci API Google, instans ini hanya memakai sumber bebas. Minta administrator TREK menambahkannya untuk rating, jam buka, dan foto.',
'places.details.aboutBrand': 'Tentang jaringan',
'places.details.aboutBrandNote': 'Menjelaskan jaringannya, bukan cabang ini.',
'places.details.fact.rating': 'Peringkat',
-2
View File
@@ -7,7 +7,6 @@ const roadtrip: TranslationStrings = {
'roadtrip.title': 'Perjalanan darat',
'roadtrip.subtitle': 'Perhentian, waktu berkendara, dan waktu tiba untuk perjalanan ini',
'roadtrip.empty.title': 'Belum ada rute',
'roadtrip.empty.body': 'Tambahkan tempat ke sebuah hari, atau pemesanan mobil dengan perhentian, dan rute akan muncul di sini.',
'roadtrip.stops.label': 'Perhentian di sepanjang jalan',
'roadtrip.stops.remove': 'Hapus perhentian',
'roadtrip.poi.title': 'Sepanjang rute',
@@ -31,7 +30,6 @@ const roadtrip: TranslationStrings = {
'roadtrip.limit.range': '{distance} sejak pengisian terakhir',
'roadtrip.limit.none': 'Belum diatur',
'roadtrip.stop.none': 'Sebuah tujuan',
'roadtrip.stop.kindHelp': 'Perhentian di jalan digambar pada rute dan tidak dihitung.',
'roadtrip.stay.mode.pause': 'Istirahat',
'roadtrip.stay.mode.night': 'Menginap',
'roadtrip.stay.checkOutDay': 'Check out',
-4
View File
@@ -92,10 +92,6 @@ const admin: TranslationStrings = {
'admin.googleCaveat.badge': 'Sconsigliato',
'admin.googleCaveat.body':
"TREK è software libero e su questo non siamo neutrali. A questa scala recensioni e foto degli esercizi comuni esistono solo da Google, ed è questo un monopolio. Il campo c'è per mancanza di alternative, non perché lo consigliamo. Ogni interrogazione va allora a Google.",
'admin.trekApi.toggleLabel':
'Indice dei luoghi TREK',
'admin.trekApi.offNote':
'Disattivato. La ricerca dei luoghi usa OpenStreetMap e Google se è configurata una chiave.',
'admin.trekApi.tagline':
'L\'indice di luoghi di TREK. Cercare senza chiave Google, senza quota e senza che nessuno conti le tue ricerche.',
'admin.trekApi.factPlaces':
-3
View File
@@ -87,7 +87,6 @@ const places: TranslationStrings = {
'places.loadingDetails': 'Caricamento dettagli del luogo…',
'places.osmHint':
'Uso della ricerca OpenStreetMap (senza foto, orari di apertura o valutazioni). Aggiungi una chiave API Google nelle impostazioni per i dettagli completi.',
'places.osmActive': 'Si usa OpenStreetMap. Una chiave API Google aggiunge valutazioni e orari.',
'places.details.title': 'Dettagli del luogo',
'places.details.empty': 'Scegli un risultato per vedere immagini e una descrizione.',
'places.details.loading': 'Caricamento dettagli...',
@@ -100,8 +99,6 @@ const places: TranslationStrings = {
'places.details.adoptBlocked': 'Svuota prima il campo descrizione',
'places.details.facts': 'Da sapere',
'places.details.nothing': 'Nessun risultato per questo luogo.',
'places.details.noKeyTitle': 'Sono disponibili più dettagli',
'places.details.noKeyHint': 'Senza una chiave API di Google questa istanza usa solo fonti libere. Chiedi all\'amministratore di TREK di aggiungerne una per valutazioni, orari e foto.',
'places.details.aboutBrand': 'Sulla catena',
'places.details.aboutBrandNote': 'Descrive la catena, non questa sede.',
'places.details.fact.rating': 'Valutazione',
-2
View File
@@ -7,7 +7,6 @@ const roadtrip: TranslationStrings = {
'roadtrip.title': 'Viaggio su strada',
'roadtrip.subtitle': 'Soste, tempi di guida e orari di arrivo di questo viaggio',
'roadtrip.empty.title': 'Nessun percorso',
'roadtrip.empty.body': 'Aggiungi luoghi a un giorno o una prenotazione auto con soste: il percorso comparirà qui.',
'roadtrip.stops.label': 'Soste lungo il tragitto',
'roadtrip.stops.remove': 'Rimuovi sosta',
'roadtrip.poi.title': 'Lungo il percorso',
@@ -31,7 +30,6 @@ const roadtrip: TranslationStrings = {
'roadtrip.limit.range': '{distance} dall’ultimo rifornimento',
'roadtrip.limit.none': 'Nessuno impostato',
'roadtrip.stop.none': 'Una destinazione',
'roadtrip.stop.kindHelp': 'Una sosta lungo il percorso viene disegnata sulla tratta e non viene contata.',
'roadtrip.stay.mode.pause': 'Sosta',
'roadtrip.stay.mode.night': 'Pernottamento',
'roadtrip.stay.checkOutDay': 'Partenza il',
-4
View File
@@ -126,10 +126,6 @@ const admin: TranslationStrings = {
'admin.googleCaveat.badge': '推奨しません',
'admin.googleCaveat.body':
'TREK はオープンソースで、この点で中立ではありません。この規模でレビューとふつうの店舗の写真があるのは Google だけで、それが独占です。この欄は代わりがないから置いてあるのであって、勧めているからではありません。使えば問い合わせはすべて Google に届きます。',
'admin.trekApi.toggleLabel':
'TREK 地点インデックス',
'admin.trekApi.offNote':
'オフです。地点検索は OpenStreetMap を使い、キーが設定されていれば Google も使います。',
'admin.trekApi.tagline':
'TREK 自前の場所インデックス。Google のキーなし、上限なし、そして誰にも検索を数えられずに探せます。',
'admin.trekApi.factPlaces':
-3
View File
@@ -87,7 +87,6 @@ const places: TranslationStrings = {
'places.loadingDetails': '詳細を読み込み中…',
'places.osmHint':
'OpenStreetMapで検索しています(写真・営業時間・評価なし)。設定でGoogle APIキーを追加すると詳細が表示されます。',
'places.osmActive': 'OpenStreetMapを使用中。Google APIキーで評価と営業時間が追加されます。',
'places.details.title': '場所の詳細',
'places.details.empty': '検索結果を選ぶと写真と説明が表示されます。',
'places.details.loading': '詳細を読み込み中...',
@@ -100,8 +99,6 @@ const places: TranslationStrings = {
'places.details.adoptBlocked': '先に説明欄を空にしてください',
'places.details.facts': '知っておくと便利',
'places.details.nothing': 'この場所の情報は見つかりませんでした。',
'places.details.noKeyTitle': 'さらに詳しい情報を表示できます',
'places.details.noKeyHint': 'Google API キーがない場合、このインスタンスは自由に使えるソースのみを利用します。評価・営業時間・写真のために、TREK の管理者にキーの追加を依頼してください。',
'places.details.aboutBrand': 'チェーンについて',
'places.details.aboutBrandNote': 'この店舗ではなくチェーン全体の説明です。',
'places.details.fact.rating': '評価',
-2
View File
@@ -7,7 +7,6 @@ const roadtrip: TranslationStrings = {
'roadtrip.title': 'ロードトリップ',
'roadtrip.subtitle': 'この旅行の立ち寄り先、運転時間、到着時刻',
'roadtrip.empty.title': 'ルートはまだありません',
'roadtrip.empty.body': '日に場所を追加するか、立ち寄り先付きの車の予約を作成すると、ここにルートが表示されます。',
'roadtrip.stops.label': '途中の立ち寄り先',
'roadtrip.stops.remove': '立ち寄り先を削除',
'roadtrip.poi.title': 'ルート沿い',
@@ -31,7 +30,6 @@ const roadtrip: TranslationStrings = {
'roadtrip.limit.range': '前回の給油から{distance}',
'roadtrip.limit.none': '未設定',
'roadtrip.stop.none': '目的地',
'roadtrip.stop.kindHelp': '途中の立ち寄りは走行線上に描かれ、停車数には数えません。',
'roadtrip.stay.mode.pause': '休憩',
'roadtrip.stay.mode.night': '宿泊',
'roadtrip.stay.checkOutDay': 'チェックアウト日',
-4
View File
@@ -131,10 +131,6 @@ const admin: TranslationStrings = {
'admin.googleCaveat.badge': '권장하지 않음',
'admin.googleCaveat.body':
'TREK은 오픈 소스이며 이 점에서 중립이 아닙니다. 이 규모에서 평점과 일반 상점 사진은 Google에만 있고, 그것이 독점입니다. 이 입력란은 대안이 없어서 있는 것이지 권해서가 아닙니다. 쓰면 모든 조회가 Google로 갑니다.',
'admin.trekApi.toggleLabel':
'TREK 장소 색인',
'admin.trekApi.offNote':
'꺼져 있습니다. 장소 검색은 OpenStreetMap을 사용하며, 키가 설정되어 있으면 Google도 사용합니다.',
'admin.trekApi.tagline':
'TREK 자체 장소 색인. Google 키 없이, 할당량 없이, 누구도 검색 횟수를 세지 않는 채로 찾습니다.',
'admin.trekApi.factPlaces':
-3
View File
@@ -86,7 +86,6 @@ const places: TranslationStrings = {
'places.loadingDetails': '장소 상세 정보 불러오는 중…',
'places.osmHint':
'OpenStreetMap 검색 사용 중 (사진, 영업 시간, 평점 없음). 전체 정보를 위해 설정에서 Google API 키를 추가하세요.',
'places.osmActive': 'OpenStreetMap을 사용 중입니다. Google API 키를 추가하면 평점과 영업시간이 표시됩니다.',
'places.details.title': '장소 세부정보',
'places.details.empty': '검색 결과를 선택하면 사진과 설명이 표시됩니다.',
'places.details.loading': '세부정보를 불러오는 중...',
@@ -99,8 +98,6 @@ const places: TranslationStrings = {
'places.details.adoptBlocked': '설명란을 먼저 비워 주세요',
'places.details.facts': '알아두면 좋아요',
'places.details.nothing': '이 장소에 대한 정보를 찾지 못했습니다.',
'places.details.noKeyTitle': '더 자세한 정보를 볼 수 있습니다',
'places.details.noKeyHint': 'Google API 키가 없으면 이 인스턴스는 무료 소스만 사용합니다. 평점, 영업시간, 사진을 보려면 TREK 관리자에게 키 추가를 요청하세요.',
'places.details.aboutBrand': '체인 소개',
'places.details.aboutBrandNote': '이 지점이 아니라 체인에 대한 설명입니다.',
'places.details.fact.rating': '평점',
-2
View File
@@ -7,7 +7,6 @@ const roadtrip: TranslationStrings = {
'roadtrip.title': '로드트립',
'roadtrip.subtitle': '이 여행의 경유지, 운전 시간 및 도착 시간',
'roadtrip.empty.title': '아직 경로가 없습니다',
'roadtrip.empty.body': '하루에 장소를 추가하거나 경유지가 있는 차량 예약을 만들면 여기에 경로가 표시됩니다.',
'roadtrip.stops.label': '가는 길의 경유지',
'roadtrip.stops.remove': '경유지 삭제',
'roadtrip.poi.title': '경로 주변',
@@ -31,7 +30,6 @@ const roadtrip: TranslationStrings = {
'roadtrip.limit.range': '마지막 주유 이후 {distance}',
'roadtrip.limit.none': '설정 안 됨',
'roadtrip.stop.none': '목적지',
'roadtrip.stop.kindHelp': '경유 정차는 주행 경로에 표시되며 정차 수에 포함되지 않습니다.',
'roadtrip.stay.mode.pause': '휴식',
'roadtrip.stay.mode.night': '숙박',
'roadtrip.stay.checkOutDay': '체크아웃 날짜',
-4
View File
@@ -93,10 +93,6 @@ const admin: TranslationStrings = {
'admin.googleCaveat.badge': 'Niet aanbevolen',
'admin.googleCaveat.body':
"TREK is open source en wij zijn hier niet neutraal. Op deze schaal bestaan beoordelingen en foto's van gewone zaken alleen bij Google, en dat is een monopolie. Het veld staat er bij gebrek aan alternatief, niet omdat wij het aanraden. Elke opvraging gaat dan naar Google.",
'admin.trekApi.toggleLabel':
'TREK-plaatsenindex',
'admin.trekApi.offNote':
'Uitgeschakeld. Zoeken naar plaatsen gebruikt OpenStreetMap, en Google als er een sleutel is ingesteld.',
'admin.trekApi.tagline':
'TREK\'s eigen plaatsenindex. Zoeken zonder Google-sleutel, zonder quotum en zonder dat iemand meetelt.',
'admin.trekApi.factPlaces':
-3
View File
@@ -87,7 +87,6 @@ const places: TranslationStrings = {
'places.loadingDetails': 'Plaatsgegevens laden…',
'places.osmHint':
"Zoeken via OpenStreetMap (geen foto's, openingstijden of beoordelingen). Voeg een Google API-sleutel toe in instellingen voor volledige details.",
'places.osmActive': 'OpenStreetMap wordt gebruikt. Een Google API-sleutel voegt beoordelingen en openingstijden toe.',
'places.details.title': 'Plaatsdetails',
'places.details.empty': 'Kies een zoekresultaat om afbeeldingen en een beschrijving te zien.',
'places.details.loading': 'Details laden...',
@@ -100,8 +99,6 @@ const places: TranslationStrings = {
'places.details.adoptBlocked': 'Maak eerst het beschrijvingsveld leeg',
'places.details.facts': 'Goed om te weten',
'places.details.nothing': 'Niets gevonden voor deze plaats.',
'places.details.noKeyTitle': 'Meer details beschikbaar',
'places.details.noKeyHint': 'Zonder Google API-sleutel gebruikt deze installatie alleen vrije bronnen. Vraag je TREK-beheerder er een toe te voegen voor beoordelingen, openingstijden en foto\'s.',
'places.details.aboutBrand': 'Over de keten',
'places.details.aboutBrandNote': 'Beschrijft de keten, niet deze vestiging.',
'places.details.fact.rating': 'Beoordeling',
-2
View File
@@ -7,7 +7,6 @@ const roadtrip: TranslationStrings = {
'roadtrip.title': 'Roadtrip',
'roadtrip.subtitle': 'Stops, rijtijden en aankomsttijden van deze reis',
'roadtrip.empty.title': 'Nog geen route',
'roadtrip.empty.body': 'Voeg plaatsen toe aan een dag of een autoboeking met stops, dan verschijnt de route hier.',
'roadtrip.stops.label': 'Stops onderweg',
'roadtrip.stops.remove': 'Stop verwijderen',
'roadtrip.poi.title': 'Langs de route',
@@ -31,7 +30,6 @@ const roadtrip: TranslationStrings = {
'roadtrip.limit.range': '{distance} sinds de laatste tankbeurt',
'roadtrip.limit.none': 'Geen ingesteld',
'roadtrip.stop.none': 'Een bestemming',
'roadtrip.stop.kindHelp': 'Een stop onderweg wordt op de rit getekend en telt niet mee.',
'roadtrip.stay.mode.pause': 'Pauze',
'roadtrip.stay.mode.night': 'Overnachten',
'roadtrip.stay.checkOutDay': 'Uitchecken op',
-4
View File
@@ -84,10 +84,6 @@ const admin: TranslationStrings = {
'admin.googleCaveat.badge': 'Niezalecane',
'admin.googleCaveat.body':
'TREK jest otwartym oprogramowaniem i nie jesteśmy tu neutralni. W tej skali oceny i zdjęcia zwykłych lokali istnieją tylko u Google, i to jest właśnie monopol. Pole jest tu z braku alternatywy, a nie dlatego, że je zalecamy. Każde zapytanie idzie wtedy do Google.',
'admin.trekApi.toggleLabel':
'Indeks miejsc TREK',
'admin.trekApi.offNote':
'Wyłączone. Wyszukiwanie miejsc korzysta z OpenStreetMap oraz z Google, jeśli ustawiono klucz.',
'admin.trekApi.tagline':
'Własny indeks miejsc TREK-a. Wyszukiwanie bez klucza Google, bez limitu i bez tego, żeby ktoś liczył twoje zapytania.',
'admin.trekApi.factPlaces':
-3
View File
@@ -77,7 +77,6 @@ const places: TranslationStrings = {
'places.loadingDetails': 'Ładowanie szczegółów miejsca…',
'places.osmHint':
'Korzystając z OpenStreetMap (brak zdjęć, godzin otwarcia czy ocen). Dodaj klucz API Google w ustawieniach aby uzyskać pełne dane.',
'places.osmActive': 'Używany jest OpenStreetMap. Klucz Google API dodaje oceny i godziny otwarcia.',
'places.details.title': 'Szczegóły miejsca',
'places.details.empty': 'Wybierz wynik wyszukiwania, aby zobaczyć zdjęcia i opis.',
'places.details.loading': 'Wczytywanie szczegółów...',
@@ -90,8 +89,6 @@ const places: TranslationStrings = {
'places.details.adoptBlocked': 'Najpierw wyczyść pole opisu',
'places.details.facts': 'Warto wiedzieć',
'places.details.nothing': 'Nic nie znaleziono dla tego miejsca.',
'places.details.noKeyTitle': 'Dostępnych jest więcej szczegółów',
'places.details.noKeyHint': 'Bez klucza API Google ta instancja korzysta tylko ze źródeł otwartych. Poproś administratora TREK o dodanie klucza, aby uzyskać oceny, godziny otwarcia i zdjęcia.',
'places.details.aboutBrand': 'O sieci',
'places.details.aboutBrandNote': 'Opisuje sieć, a nie ten lokal.',
'places.details.fact.rating': 'Ocena',
-2
View File
@@ -7,7 +7,6 @@ const roadtrip: TranslationStrings = {
'roadtrip.title': 'Podróż samochodowa',
'roadtrip.subtitle': 'Przystanki, czasy jazdy i godziny przyjazdu tej podróży',
'roadtrip.empty.title': 'Brak trasy',
'roadtrip.empty.body': 'Dodaj miejsca do dnia albo rezerwację samochodu z przystankami, a trasa pojawi się tutaj.',
'roadtrip.stops.label': 'Przystanki po drodze',
'roadtrip.stops.remove': 'Usuń przystanek',
'roadtrip.poi.title': 'Wzdłuż trasy',
@@ -31,7 +30,6 @@ const roadtrip: TranslationStrings = {
'roadtrip.limit.range': '{distance} od ostatniego tankowania',
'roadtrip.limit.none': 'Brak ustawionych',
'roadtrip.stop.none': 'Cel podróży',
'roadtrip.stop.kindHelp': 'Postój po drodze rysowany jest na trasie i nie jest liczony.',
'roadtrip.stay.mode.pause': 'Postój',
'roadtrip.stay.mode.night': 'Nocleg',
'roadtrip.stay.checkOutDay': 'Wymeldowanie',
-4
View File
@@ -93,10 +93,6 @@ const admin: TranslationStrings = {
'admin.googleCaveat.badge': 'Не рекомендуется',
'admin.googleCaveat.body':
'TREK с открытым исходным кодом, и мы здесь не нейтральны. В таком масштабе отзывы и фотографии обычных заведений есть только у Google, и это и есть монополия. Поле здесь из-за отсутствия альтернативы, а не потому, что мы его советуем. Тогда каждый запрос уходит в Google.',
'admin.trekApi.toggleLabel':
'Индекс мест TREK',
'admin.trekApi.offNote':
'Выключено. Поиск мест использует OpenStreetMap, а также Google, если задан ключ.',
'admin.trekApi.tagline':
'Собственный указатель мест TREK. Поиск без ключа Google, без квоты и без того, чтобы кто-то считал ваши запросы.',
'admin.trekApi.factPlaces':
-3
View File
@@ -87,7 +87,6 @@ const places: TranslationStrings = {
'places.loadingDetails': 'Загрузка данных о месте…',
'places.osmHint':
'Поиск через OpenStreetMap (без фото, часов работы и рейтингов). Добавьте API-ключ Google в настройках для полной информации.',
'places.osmActive': 'Используется OpenStreetMap. Ключ Google API добавит рейтинги и часы работы.',
'places.details.title': 'Сведения о месте',
'places.details.empty': 'Выберите результат поиска, чтобы увидеть изображения и описание.',
'places.details.loading': 'Загрузка сведений...',
@@ -100,8 +99,6 @@ const places: TranslationStrings = {
'places.details.adoptBlocked': 'Сначала очистите поле описания',
'places.details.facts': 'Полезно знать',
'places.details.nothing': 'Для этого места ничего не найдено.',
'places.details.noKeyTitle': 'Доступно больше подробностей',
'places.details.noKeyHint': 'Без ключа Google API этот экземпляр использует только свободные источники. Попросите администратора TREK добавить ключ для оценок, часов работы и фотографий.',
'places.details.aboutBrand': 'О сети',
'places.details.aboutBrandNote': 'Описывает сеть, а не это заведение.',
'places.details.fact.rating': 'Рейтинг',
-2
View File
@@ -7,7 +7,6 @@ const roadtrip: TranslationStrings = {
'roadtrip.title': 'Автопутешествие',
'roadtrip.subtitle': 'Остановки, время в пути и время прибытия для этой поездки',
'roadtrip.empty.title': 'Маршрута пока нет',
'roadtrip.empty.body': 'Добавьте места в день или бронирование авто с остановками — маршрут появится здесь.',
'roadtrip.stops.label': 'Остановки в пути',
'roadtrip.stops.remove': 'Удалить остановку',
'roadtrip.poi.title': 'Вдоль маршрута',
@@ -31,7 +30,6 @@ const roadtrip: TranslationStrings = {
'roadtrip.limit.range': '{distance} с последней заправки',
'roadtrip.limit.none': 'Не заданы',
'roadtrip.stop.none': 'Пункт назначения',
'roadtrip.stop.kindHelp': 'Остановка в пути рисуется на маршруте и не учитывается в счёте.',
'roadtrip.stay.mode.pause': 'Остановка',
'roadtrip.stay.mode.night': 'Ночёвка',
'roadtrip.stay.checkOutDay': 'Выезд',
-4
View File
@@ -135,10 +135,6 @@ const admin: TranslationStrings = {
'admin.googleCaveat.badge': 'Avrådes',
'admin.googleCaveat.body':
'TREK är öppen källkod och vi är inte neutrala här. I den här skalan finns omdömen och foton på vanliga verksamheter bara hos Google, och det är ett monopol. Fältet finns i brist på alternativ, inte för att vi rekommenderar det. Varje uppslagning går då till Google.',
'admin.trekApi.toggleLabel':
'TREK:s platsindex',
'admin.trekApi.offNote':
'Avstängt. Platssökningen använder OpenStreetMap och Google om en nyckel är angiven.',
'admin.trekApi.tagline':
'TREK:s eget platsregister. Sök utan Google-nyckel, utan kvot och utan att någon räknar dina sökningar.',
'admin.trekApi.factPlaces':
-3
View File
@@ -87,7 +87,6 @@ const places: TranslationStrings = {
'places.loadingDetails': 'Hämtar information om platsen…',
'places.osmHint':
'Använder OpenStreetMap-sökningen (inga bilder, öppettider eller betyg). Lägg till en Google API-nyckel i inställningarna för fullständig information.',
'places.osmActive': 'OpenStreetMap används. En Google API-nyckel lägger till betyg och öppettider.',
'places.details.title': 'Platsdetaljer',
'places.details.empty': 'Välj ett sökresultat för att se bilder och en beskrivning.',
'places.details.loading': 'Laddar detaljer...',
@@ -100,8 +99,6 @@ const places: TranslationStrings = {
'places.details.adoptBlocked': 'Töm beskrivningsfältet först',
'places.details.facts': 'Bra att veta',
'places.details.nothing': 'Inget hittades för den här platsen.',
'places.details.noKeyTitle': 'Fler detaljer finns tillgängliga',
'places.details.noKeyHint': 'Utan en Google API-nyckel använder den här instansen bara fria källor. Be din TREK-administratör lägga till en för betyg, öppettider och foton.',
'places.details.aboutBrand': 'Om kedjan',
'places.details.aboutBrandNote': 'Beskriver kedjan, inte det här stället.',
'places.details.fact.rating': 'Betyg',
-2
View File
@@ -7,7 +7,6 @@ const roadtrip: TranslationStrings = {
'roadtrip.title': 'Bilresa',
'roadtrip.subtitle': 'Stopp, körtider och ankomsttider för den här resan',
'roadtrip.empty.title': 'Ingen rutt ännu',
'roadtrip.empty.body': 'Lägg till platser på en dag eller en bilbokning med stopp, så visas rutten här.',
'roadtrip.stops.label': 'Stopp längs vägen',
'roadtrip.stops.remove': 'Ta bort stopp',
'roadtrip.poi.title': 'Längs rutten',
@@ -31,7 +30,6 @@ const roadtrip: TranslationStrings = {
'roadtrip.limit.range': '{distance} sedan senaste tankningen',
'roadtrip.limit.none': 'Inga angivna',
'roadtrip.stop.none': 'Ett resmål',
'roadtrip.stop.kindHelp': 'Ett stopp på vägen ritas på körningen och räknas inte.',
'roadtrip.stay.mode.pause': 'Paus',
'roadtrip.stay.mode.night': 'Övernattning',
'roadtrip.stay.checkOutDay': 'Utcheckning',
-4
View File
@@ -135,10 +135,6 @@ const admin: TranslationStrings = {
'admin.googleCaveat.badge': 'Önerilmez',
'admin.googleCaveat.body':
"TREK açık kaynaklıdır ve burada tarafsız değiliz. Bu ölçekte değerlendirmeler ve sıradan işletme fotoğrafları yalnızca Google'da vardır; tekel budur. Alan, başka seçenek olmadığı için burada; önerdiğimiz için değil. O zaman her sorgu Google'a gider.",
'admin.trekApi.toggleLabel':
'TREK yer dizini',
'admin.trekApi.offNote':
'Kapalı. Yer araması OpenStreetMap kullanır, anahtar tanımlıysa Google da kullanılır.',
'admin.trekApi.tagline':
'TREK\'in kendi yer dizini. Google anahtarı olmadan, kota olmadan ve kimse aramalarınızı saymadan arama.',
'admin.trekApi.factPlaces':
-3
View File
@@ -89,7 +89,6 @@ const places: TranslationStrings = {
'places.loadingDetails': 'Yer ayrıntıları yükleniyor…',
'places.osmHint':
'OpenStreetMap aramasını kullanma (fotoğraf, açılış saatleri veya derecelendirme yok). Tüm ayrıntılar için ayarlara bir Google API anahtarı ekleyin.',
'places.osmActive': 'OpenStreetMap kullanılıyor. Google API anahtarı puan ve çalışma saatleri ekler.',
'places.details.title': 'Yer ayrıntıları',
'places.details.empty': 'Görseller ve açıklama için bir arama sonucu seç.',
'places.details.loading': 'Ayrıntılar yükleniyor...',
@@ -102,8 +101,6 @@ const places: TranslationStrings = {
'places.details.adoptBlocked': 'Önce açıklama alanını boşalt',
'places.details.facts': 'Bilmekte fayda var',
'places.details.nothing': 'Bu yer için bir şey bulunamadı.',
'places.details.noKeyTitle': 'Daha fazla ayrıntı mevcut',
'places.details.noKeyHint': 'Google API anahtarı olmadan bu örnek yalnızca özgür kaynakları kullanır. Puanlar, açılış saatleri ve fotoğraflar için TREK yöneticinizden bir anahtar eklemesini isteyin.',
'places.details.aboutBrand': 'Zincir hakkında',
'places.details.aboutBrandNote': 'Bu şubeyi değil, zinciri anlatır.',
'places.details.fact.rating': 'Puan',
-2
View File
@@ -7,7 +7,6 @@ const roadtrip: TranslationStrings = {
'roadtrip.title': 'Yol gezisi',
'roadtrip.subtitle': 'Bu geziye ait duraklar, sürüş süreleri ve varış saatleri',
'roadtrip.empty.title': 'Henüz rota yok',
'roadtrip.empty.body': 'Bir güne yer ekleyin ya da duraklı bir araç rezervasyonu oluşturun; rota burada görünür.',
'roadtrip.stops.label': 'Yol üzerindeki duraklar',
'roadtrip.stops.remove': 'Durağı kaldır',
'roadtrip.poi.title': 'Rota boyunca',
@@ -31,7 +30,6 @@ const roadtrip: TranslationStrings = {
'roadtrip.limit.range': 'son yakıt alımından beri {distance}',
'roadtrip.limit.none': 'Ayarlanmadı',
'roadtrip.stop.none': 'Bir varış noktası',
'roadtrip.stop.kindHelp': 'Yol üzerindeki mola sürüşe çizilir ve sayıma girmez.',
'roadtrip.stay.mode.pause': 'Mola',
'roadtrip.stay.mode.night': 'Konaklama',
'roadtrip.stay.checkOutDay': 'Çıkış günü',
-4
View File
@@ -93,10 +93,6 @@ const admin: TranslationStrings = {
'admin.googleCaveat.badge': 'Не рекомендовано',
'admin.googleCaveat.body':
'TREK має відкритий код, і ми тут не нейтральні. У такому масштабі відгуки та світлини звичайних закладів є лише в Google, і це і є монополія. Поле тут через брак альтернативи, а не тому, що ми його радимо. Тоді кожен запит іде до Google.',
'admin.trekApi.toggleLabel':
'Індекс місць TREK',
'admin.trekApi.offNote':
'Вимкнено. Пошук місць використовує OpenStreetMap, а також Google, якщо задано ключ.',
'admin.trekApi.tagline':
'Власний покажчик місць TREK. Пошук без ключа Google, без квоти й без того, щоб хтось рахував ваші запити.',
'admin.trekApi.factPlaces':
-3
View File
@@ -87,7 +87,6 @@ const places: TranslationStrings = {
'places.loadingDetails': 'Завантаження даних про місце…',
'places.osmHint':
'Пошук через OpenStreetMap (без фото, годин роботи та рейтингів). Додайте API-ключ Google у налаштуваннях для повної інформації.',
'places.osmActive': 'Використовується OpenStreetMap. Ключ Google API додасть рейтинги та години роботи.',
'places.details.title': 'Відомості про місце',
'places.details.empty': 'Виберіть результат пошуку, щоб побачити зображення та опис.',
'places.details.loading': 'Завантаження відомостей...',
@@ -100,8 +99,6 @@ const places: TranslationStrings = {
'places.details.adoptBlocked': 'Спершу очистіть поле опису',
'places.details.facts': 'Корисно знати',
'places.details.nothing': 'Для цього місця нічого не знайдено.',
'places.details.noKeyTitle': 'Доступно більше деталей',
'places.details.noKeyHint': 'Без ключа Google API цей екземпляр використовує лише вільні джерела. Попросіть адміністратора TREK додати ключ, щоб отримати оцінки, години роботи та фото.',
'places.details.aboutBrand': 'Про мережу',
'places.details.aboutBrandNote': 'Описує мережу, а не цей заклад.',
'places.details.fact.rating': 'Рейтинг',
-2
View File
@@ -7,7 +7,6 @@ const roadtrip: TranslationStrings = {
'roadtrip.title': 'Автоподорож',
'roadtrip.subtitle': 'Зупинки, час у дорозі та час прибуття для цієї подорожі',
'roadtrip.empty.title': 'Маршруту ще немає',
'roadtrip.empty.body': 'Додайте місця до дня або бронювання авто із зупинками — маршрут з’явиться тут.',
'roadtrip.stops.label': 'Зупинки в дорозі',
'roadtrip.stops.remove': 'Видалити зупинку',
'roadtrip.poi.title': 'Уздовж маршруту',
@@ -31,7 +30,6 @@ const roadtrip: TranslationStrings = {
'roadtrip.limit.range': '{distance} від останньої заправки',
'roadtrip.limit.none': 'Не задано',
'roadtrip.stop.none': 'Пункт призначення',
'roadtrip.stop.kindHelp': 'Зупинка в дорозі малюється на маршруті й не рахується.',
'roadtrip.stay.mode.pause': 'Зупинка',
'roadtrip.stay.mode.night': 'Ночівля',
'roadtrip.stay.checkOutDay': 'Виїзд',
-4
View File
@@ -134,10 +134,6 @@ const admin: TranslationStrings = {
'admin.googleCaveat.badge': 'Không khuyến nghị',
'admin.googleCaveat.body':
'TREK là phần mềm nguồn mở và ở đây chúng tôi không trung lập. Ở quy mô này, đánh giá và ảnh của các cửa hàng thông thường chỉ có ở Google, và đó là độc quyền. Ô này có mặt vì không có lựa chọn khác, không phải vì chúng tôi khuyến nghị. Khi đó mọi truy vấn đều đi tới Google.',
'admin.trekApi.toggleLabel':
'Chỉ mục địa điểm TREK',
'admin.trekApi.offNote':
'Đã tắt. Tìm kiếm địa điểm dùng OpenStreetMap, và Google nếu đã đặt khóa.',
'admin.trekApi.tagline':
'Chỉ mục địa điểm của riêng TREK. Tìm kiếm không cần khóa Google, không hạn mức, và không ai đếm lượt tìm của bạn.',
'admin.trekApi.factPlaces':
-3
View File
@@ -87,7 +87,6 @@ const places: TranslationStrings = {
'places.loadingDetails': 'Đang tải chi tiết địa điểm…',
'places.osmHint':
'Sử dụng tìm kiếm OpenStreetMap (không có ảnh, giờ mở cửa hoặc xếp hạng). Thêm khóa Google API trong cài đặt để biết đầy đủ chi tiết.',
'places.osmActive': 'Đang dùng OpenStreetMap. Khóa API Google bổ sung đánh giá và giờ mở cửa.',
'places.details.title': 'Chi tiết địa điểm',
'places.details.empty': 'Chọn một kết quả tìm kiếm để xem hình ảnh và mô tả.',
'places.details.loading': 'Đang tải chi tiết...',
@@ -100,8 +99,6 @@ const places: TranslationStrings = {
'places.details.adoptBlocked': 'Hãy xóa trống ô mô tả trước',
'places.details.facts': 'Thông tin hữu ích',
'places.details.nothing': 'Không tìm thấy gì cho địa điểm này.',
'places.details.noKeyTitle': 'Có thêm thông tin chi tiết',
'places.details.noKeyHint': 'Không có khóa API Google, phiên bản này chỉ dùng nguồn miễn phí. Hãy đề nghị quản trị viên TREK thêm khóa để có xếp hạng, giờ mở cửa và ảnh.',
'places.details.aboutBrand': 'Về chuỗi',
'places.details.aboutBrandNote': 'Mô tả chuỗi, không phải chi nhánh này.',
'places.details.fact.rating': 'Đánh giá',
-2
View File
@@ -7,7 +7,6 @@ const roadtrip: TranslationStrings = {
'roadtrip.title': 'Chuyến đi đường bộ',
'roadtrip.subtitle': 'Điểm dừng, thời gian lái xe và giờ đến của chuyến đi này',
'roadtrip.empty.title': 'Chưa có lộ trình',
'roadtrip.empty.body': 'Thêm địa điểm vào một ngày hoặc đặt xe có điểm dừng, lộ trình sẽ hiện ở đây.',
'roadtrip.stops.label': 'Điểm dừng dọc đường',
'roadtrip.stops.remove': 'Xóa điểm dừng',
'roadtrip.poi.title': 'Dọc theo lộ trình',
@@ -31,7 +30,6 @@ const roadtrip: TranslationStrings = {
'roadtrip.limit.range': '{distance} kể từ lần đổ nhiên liệu cuối',
'roadtrip.limit.none': 'Chưa đặt',
'roadtrip.stop.none': 'Một điểm đến',
'roadtrip.stop.kindHelp': 'Điểm dừng dọc đường được vẽ trên chặng và không tính vào số điểm dừng.',
'roadtrip.stay.mode.pause': 'Nghỉ',
'roadtrip.stay.mode.night': 'Qua đêm',
'roadtrip.stay.checkOutDay': 'Trả phòng ngày',
-4
View File
@@ -130,10 +130,6 @@ const admin: TranslationStrings = {
'admin.googleCaveat.badge': '不建議',
'admin.googleCaveat.body':
'TREK 是開源軟體,在這件事上我們並不中立。在這個規模上,評分和一般店家的照片只有 Google 有,這就是壟斷。這個欄位存在是因為沒有替代品,而不是因為我們推薦。填了它,每次查詢都會送往 Google。',
'admin.trekApi.toggleLabel':
'TREK 地點索引',
'admin.trekApi.offNote':
'已關閉。地點搜尋使用 OpenStreetMap;若已設定金鑰,也會使用 Google。',
'admin.trekApi.tagline':
'TREK 自己的地點索引。不用 Google 金鑰,沒有配額,也沒有人統計你搜了什麼。',
'admin.trekApi.factPlaces':
-3
View File
@@ -85,7 +85,6 @@ const places: TranslationStrings = {
'places.mapsSearchError': '地點搜尋失敗。',
'places.loadingDetails': '正在載入地點詳情…',
'places.osmHint': '使用 OpenStreetMap 搜尋(無照片、營業時間或評分)。在設定中新增 Google API 金鑰以獲取完整資訊。',
'places.osmActive': '正在使用 OpenStreetMap。加入 Google API 金鑰可取得評分與營業時間。',
'places.details.title': '地點詳細資料',
'places.details.empty': '選擇一筆搜尋結果即可查看圖片與描述。',
'places.details.loading': '正在載入詳細資料...',
@@ -98,8 +97,6 @@ const places: TranslationStrings = {
'places.details.adoptBlocked': '請先清空描述欄位',
'places.details.facts': '值得一看',
'places.details.nothing': '找不到此地點的資訊。',
'places.details.noKeyTitle': '可取得更多詳細資訊',
'places.details.noKeyHint': '沒有 Google API 金鑰時,此執行個體僅使用免費來源。請聯絡 TREK 管理員新增金鑰,以取得評分、營業時間與照片。',
'places.details.aboutBrand': '關於該連鎖品牌',
'places.details.aboutBrandNote': '描述的是連鎖品牌,而非本分店。',
'places.details.fact.rating': '評分',
-2
View File
@@ -7,7 +7,6 @@ const roadtrip: TranslationStrings = {
'roadtrip.title': '公路旅行',
'roadtrip.subtitle': '本次行程的停靠點、駕駛時間與抵達時間',
'roadtrip.empty.title': '尚無路線',
'roadtrip.empty.body': '為某一天新增地點,或建立含停靠點的租車預訂,路線就會顯示在這裡。',
'roadtrip.stops.label': '沿途停靠點',
'roadtrip.stops.remove': '移除停靠點',
'roadtrip.poi.title': '沿線',
@@ -31,7 +30,6 @@ const roadtrip: TranslationStrings = {
'roadtrip.limit.range': '距上次加油 {distance}',
'roadtrip.limit.none': '未設定',
'roadtrip.stop.none': '目的地',
'roadtrip.stop.kindHelp': '途中停靠會畫在行程線上,且不計入停靠數。',
'roadtrip.stay.mode.pause': '停留',
'roadtrip.stay.mode.night': '過夜',
'roadtrip.stay.checkOutDay': '退房日',
-4
View File
@@ -91,10 +91,6 @@ const admin: TranslationStrings = {
'admin.googleCaveat.badge': '不推荐',
'admin.googleCaveat.body':
'TREK 是开源软件,在这件事上我们并不中立。在这个规模上,评分和普通店铺的照片只有 Google 有,这就是垄断。这个输入框存在是因为没有替代品,而不是因为我们推荐。填了它,每次查询都会发往 Google。',
'admin.trekApi.toggleLabel':
'TREK 地点索引',
'admin.trekApi.offNote':
'已关闭。地点搜索使用 OpenStreetMap;如果已配置密钥,也会使用 Google。',
'admin.trekApi.tagline':
'TREK 自己的地点索引。不用 Google 密钥,没有配额,也没有人统计你搜了什么。',
'admin.trekApi.factPlaces':
-3
View File
@@ -85,7 +85,6 @@ const places: TranslationStrings = {
'places.mapsSearchError': '地点搜索失败。',
'places.loadingDetails': '正在加载地点详情…',
'places.osmHint': '使用 OpenStreetMap 搜索(无照片、营业时间或评分)。在设置中添加 Google API 密钥以获取完整信息。',
'places.osmActive': '正在使用 OpenStreetMap。添加 Google API 密钥可获得评分和营业时间。',
'places.details.title': '地点详情',
'places.details.empty': '选择一条搜索结果即可查看图片和描述。',
'places.details.loading': '正在加载详情...',
@@ -98,8 +97,6 @@ const places: TranslationStrings = {
'places.details.adoptBlocked': '请先清空描述字段',
'places.details.facts': '值得一看',
'places.details.nothing': '未找到该地点的信息。',
'places.details.noKeyTitle': '可获取更多详情',
'places.details.noKeyHint': '没有 Google API 密钥时,此实例仅使用免费来源。请联系 TREK 管理员添加密钥,以获取评分、营业时间和照片。',
'places.details.aboutBrand': '关于该连锁品牌',
'places.details.aboutBrandNote': '描述的是连锁品牌,而非本门店。',
'places.details.fact.rating': '评分',
-2
View File
@@ -7,7 +7,6 @@ const roadtrip: TranslationStrings = {
'roadtrip.title': '公路旅行',
'roadtrip.subtitle': '本次行程的停靠点、驾驶时间和到达时间',
'roadtrip.empty.title': '还没有路线',
'roadtrip.empty.body': '为某一天添加地点,或创建带停靠点的租车预订,路线就会显示在这里。',
'roadtrip.stops.label': '沿途停靠点',
'roadtrip.stops.remove': '移除停靠点',
'roadtrip.poi.title': '沿线',
@@ -31,7 +30,6 @@ const roadtrip: TranslationStrings = {
'roadtrip.limit.range': '距上次加油 {distance}',
'roadtrip.limit.none': '未设置',
'roadtrip.stop.none': '目的地',
'roadtrip.stop.kindHelp': '途中停靠会画在行程线上,且不计入停靠数。',
'roadtrip.stay.mode.pause': '停留',
'roadtrip.stay.mode.night': '过夜',
'roadtrip.stay.checkOutDay': '退房日',