diff --git a/client/src/components/Admin/GoogleDailyLimitRow.tsx b/client/src/components/Admin/GoogleDailyLimitRow.tsx
index db462c445..e98d558c5 100644
--- a/client/src/components/Admin/GoogleDailyLimitRow.tsx
+++ b/client/src/components/Admin/GoogleDailyLimitRow.tsx
@@ -5,8 +5,8 @@ import { useGoogleQuota } from './useGoogleQuota'
/**
* The daily ceiling on Google calls (#1582), as one row of the Google options:
- * the number on the right with today's count under it, a save button only while
- * there is something to save, and a warning once the day is used up.
+ * today's count as a badge beside the name, the number on the right, a save
+ * button only while there is something to save.
*/
export default function GoogleDailyLimitRow() {
const { t } = useTranslation()
@@ -15,10 +15,28 @@ export default function GoogleDailyLimitRow() {
return (
-
{t('admin.googleQuota.title')}
+
+
{t('admin.googleQuota.title')}
+ {/* Today's count beside the name it belongs to: green while there is room, amber once the day is used up. */}
+ {status && (
+ status.exhausted ? (
+
+
+ {t('admin.googleQuota.reached', { used: status.used_today })}
+
+ ) : (
+
+
+ {status.daily_limit == null
+ ? t('admin.googleQuota.usedToday', { used: status.used_today })
+ : t('admin.googleQuota.usedOfLimit', { used: status.used_today, limit: status.daily_limit })}
+
+ )
+ )}
+
{t('admin.googleQuota.subtitle')}
-
+
)}
- {status && (
- status.exhausted ? (
-
-
- {t('admin.googleQuota.reached', { used: status.used_today })}
-
- ) : (
-
- {status.daily_limit == null
- ? t('admin.googleQuota.usedToday', { used: status.used_today })
- : t('admin.googleQuota.usedOfLimit', { used: status.used_today, limit: status.daily_limit })}
-
- )
- )}
)
diff --git a/client/src/components/Planner/PlaceContactFields.test.tsx b/client/src/components/Planner/PlaceContactFields.test.tsx
index de08de5bd..8b0a6f2f0 100644
--- a/client/src/components/Planner/PlaceContactFields.test.tsx
+++ b/client/src/components/Planner/PlaceContactFields.test.tsx
@@ -4,11 +4,12 @@ import { useState } from 'react'
import userEvent from '@testing-library/user-event'
import { render, screen } from '../../../tests/helpers/render'
import { PlaceContactFields } from './PlaceContactFields'
+import type { PlaceOpeningHours } from '@trek/shared'
-function Harness({ onChange }: { onChange: (field: string, value: string) => void }) {
- const [state, setState] = useState({ phone: '', email: '', opening_hours: '' })
+function Harness({ onChange, start = '', suggested }: { onChange: (field: string, value: string) => void; start?: string; suggested?: PlaceOpeningHours | null }) {
+ const [state, setState] = useState({ phone: '', email: '', opening_hours: start })
return (
-
{ onChange(field, value); setState(s => ({ ...s, [field]: value })) }} />
)
}
@@ -44,4 +45,33 @@ describe('PlaceContactFields (#2472)', () => {
expect(onChange).toHaveBeenLastCalledWith('opening_hours', '')
expect(screen.getByRole('button', { name: 'Add opening hours' })).toBeInTheDocument()
})
+
+ it('FE-PLANNER-CONTACT-003: a time can be typed key by key without the field resetting', async () => {
+ const user = userEvent.setup()
+ const onChange = vi.fn()
+ render( )
+ await user.click(screen.getByRole('button', { name: 'Add opening hours' }))
+ const opens = screen.getAllByRole('textbox', { name: /Monday opens/ })
+ await user.type(opens[0], '0930')
+ expect(opens[0]).toHaveValue('09:30')
+ expect(JSON.parse(onChange.mock.lastCall![1])[0]).toEqual({ closed: false, open: '09:30' })
+ })
+
+ it('FE-PLANNER-CONTACT-004: looked-up hours fill an empty place, and an edited one offers to take them over', async () => {
+ const user = userEvent.setup()
+ const looked: PlaceOpeningHours = [{ closed: false, open: '10:00', close: '16:00' }, ...Array.from({ length: 6 }, () => ({ closed: true }))]
+ const onChange = vi.fn()
+ const { unmount } = render( )
+ expect(JSON.parse(onChange.mock.lastCall![1])).toEqual(looked)
+ expect(screen.queryByRole('button', { name: 'Take over from the place details' })).toBeNull()
+ unmount()
+
+ onChange.mockClear()
+ const own = JSON.stringify([{ closed: false, open: '08:00', close: '09:00' }, ...Array.from({ length: 6 }, () => ({ closed: false }))])
+ render( )
+ expect(onChange).not.toHaveBeenCalled()
+ await user.click(screen.getByRole('button', { name: 'Take over from the place details' }))
+ expect(JSON.parse(onChange.mock.lastCall![1])).toEqual(looked)
+ })
})
+
diff --git a/client/src/components/Planner/PlaceContactFields.tsx b/client/src/components/Planner/PlaceContactFields.tsx
index b18ceb6d3..7d6452c39 100644
--- a/client/src/components/Planner/PlaceContactFields.tsx
+++ b/client/src/components/Planner/PlaceContactFields.tsx
@@ -1,11 +1,11 @@
-import { useId, useState } from 'react'
-import { Copy, Mail, Phone, X } from 'lucide-react'
+import { useEffect, useId, useRef, useState } from 'react'
+import { Copy, Mail, Phone, RotateCcw, X } from 'lucide-react'
import type { PlaceOpeningHours } from '@trek/shared'
import CustomTimePicker from '../shared/CustomTimePicker'
import { Tooltip } from '../shared/Tooltip'
import { DialogSection, fs } from '../shared/DialogShell'
import { AddRowButton, EditorField, GRID_2, INPUT } from '../shared/dialogParts'
-import { readWeek, weekdayNames, writeWeek } from './placeHours'
+import { cleanWeek, readWeek, weekdayNames, writeWeek } from './placeHours'
import { useTranslation } from '../../i18n'
interface Props {
@@ -16,6 +16,12 @@ interface Props {
/** The stored hours text (JSON), empty when the place has none of its own. */
openingHours: string
onChange: (field: 'phone' | 'email' | 'opening_hours', value: string) => void
+ /**
+ * The hours the place details column looked up, as a week. A place without
+ * hours of its own takes them over on its own; one that has some gets a
+ * button to take them over instead.
+ */
+ suggestedHours?: PlaceOpeningHours | null
}
/**
@@ -25,19 +31,47 @@ interface Props {
* behind one row until somebody wants them, so a place without any does not
* carry a seven-row grid.
*/
-export function PlaceContactFields({ id: anchorId, phone, email, openingHours, onChange }: Props) {
+export function PlaceContactFields({ id: anchorId, phone, email, openingHours, onChange, suggestedHours }: Props) {
const { t, locale } = useTranslation()
const id = useId()
- const week = readWeek(openingHours)
+ // The editor's own draft, so a half-typed time ("09:3") stays on screen while
+ // only finished times reach the place.
+ const [week, setDraft] = useState(() => readWeek(openingHours))
+ const written = useRef(openingHours)
const [editing, setEditing] = useState(() => openingHours !== '')
const names = weekdayNames(locale, 'short')
const longNames = weekdayNames(locale, 'long')
- const setWeek = (next: PlaceOpeningHours) => onChange('opening_hours', writeWeek(next))
+ // A value from outside (a picked search result, a reopened place) replaces the draft.
+ useEffect(() => {
+ if (openingHours === written.current) return
+ written.current = openingHours
+ setDraft(readWeek(openingHours))
+ if (openingHours) setEditing(true)
+ }, [openingHours])
+
+ const setWeek = (next: PlaceOpeningHours) => {
+ setDraft(next)
+ const text = writeWeek(cleanWeek(next))
+ written.current = text
+ onChange('opening_hours', text)
+ }
const setDay = (i: number, patch: Partial) =>
setWeek(week.map((day, d) => (d === i ? { ...day, ...patch } : day)))
const copyFirstToAll = () => setWeek(week.map(() => ({ ...week[0] })))
- const clearHours = () => { onChange('opening_hours', ''); setEditing(false) }
+ const clearHours = () => { setWeek(readWeek('')); setEditing(false) }
+
+ // Looked-up hours are taken over by a place that has none yet, once per lookup.
+ const adopted = useRef(null)
+ useEffect(() => {
+ if (!suggestedHours || adopted.current === suggestedHours) return
+ adopted.current = suggestedHours
+ if (written.current) return
+ setWeek(suggestedHours)
+ setEditing(true)
+ // setWeek only reads refs and setters; the lookup is the trigger.
+ }, [suggestedHours])
+ const canTakeOver = !!suggestedHours && writeWeek(suggestedHours) !== writeWeek(cleanWeek(week))
return (
@@ -62,6 +96,14 @@ export function PlaceContactFields({ id: anchorId, phone, email, openingHours, o
label={t('inspector.openingHours')}
action={editing ? (
+ {canTakeOver && (
+
+ setWeek(suggestedHours!)} aria-label={t('places.hoursFromDetails')}
+ className="grid h-7 w-7 place-items-center rounded-full text-content-faint hover:bg-surface-hover hover:text-content">
+
+
+
+ )}
diff --git a/client/src/components/Planner/PlaceDetailsColumn.tsx b/client/src/components/Planner/PlaceDetailsColumn.tsx
index 47e924811..137c579fe 100644
--- a/client/src/components/Planner/PlaceDetailsColumn.tsx
+++ b/client/src/components/Planner/PlaceDetailsColumn.tsx
@@ -50,6 +50,8 @@ interface PlaceDetailsColumnProps {
selectedImageUrl?: string
onPickImage: (url: string | null) => void
onAdoptDescription: (text: string) => void
+ /** The looked-up opening hours, once they arrive, so the form can take them over (#2472). */
+ onHours?: (hours: PlaceHours) => void
/** True once the form's description field has something in it. */
hasDescription: boolean
language: string
@@ -152,6 +154,7 @@ export default function PlaceDetailsColumn({
selectedImageUrl,
onPickImage,
onAdoptDescription,
+ onHours,
hasDescription,
language,
timeFormat = '24h',
@@ -244,6 +247,13 @@ export default function PlaceDetailsColumn({
useEffect(() => () => abortRef.current?.abort(), [])
+ // Handed to the form once per answer; a ref so a new callback identity does not re-send it.
+ const onHoursRef = useRef(onHours)
+ useEffect(() => { onHoursRef.current = onHours })
+ useEffect(() => {
+ if (state === 'ready' && data?.hours) onHoursRef.current?.(data.hours)
+ }, [state, data])
+
const isEmpty =
state === 'ready' &&
!data?.disabled &&
diff --git a/client/src/components/Planner/PlaceFormModal.tsx b/client/src/components/Planner/PlaceFormModal.tsx
index f3c06c6a8..97be93ad6 100644
--- a/client/src/components/Planner/PlaceFormModal.tsx
+++ b/client/src/components/Planner/PlaceFormModal.tsx
@@ -16,6 +16,8 @@ import { Search, Paperclip, X, AlertTriangle, Loader2, Plus, RotateCcw, MapPin,
import { useTranslation } from '../../i18n'
import CustomTimePicker from '../shared/CustomTimePicker'
import { PlaceContactFields } from './PlaceContactFields'
+import { weekFromPeriods } from './placeHours'
+import type { PlaceOpeningHours } from '@trek/shared'
import {
DEFAULT_FORM, endsBeforeStart, findDuplicatePlace, formPin, isMapUrl, mergeResult, parseCoordinatePair, timeCollisions,
type PlaceFormData, type ResultField,
@@ -167,6 +169,8 @@ function usePlaceFormModal(props: PlaceFormModalProps) {
// The query the last full search found nothing for (#2472): the cue to add the
// place by hand instead of a silent empty list.
const [emptySearch, setEmptySearch] = useState(null)
+ // The hours the details column looked up, as a week the contact block can take over (#2472).
+ const [detailsHours, setDetailsHours] = useState(null)
/**
* The leg of the drive the traveller picked, or empty while the projection's own
* answer stands. Empty rather than seeded, because there is nothing to project onto
@@ -823,6 +827,7 @@ function usePlaceFormModal(props: PlaceFormModalProps) {
isSaving,
pendingFiles,
emptySearch, setEmptySearch,
+ detailsHours, setDetailsHours,
fileRef,
acSuggestions,
setAcSuggestions,
@@ -1018,6 +1023,7 @@ export default function PlaceFormModal(props: PlaceFormModalProps) {
selectedImageUrl={form.image_url}
onPickImage={(url) => setForm(prev => ({ ...prev, image_url: url ?? undefined }))}
onAdoptDescription={(text) => setForm(prev => ({ ...prev, description: text }))}
+ onHours={(hours) => S.setDetailsHours(weekFromPeriods(hours.periods))}
hasDescription={!!form.description.trim()}
language={language}
timeFormat={S.timeFormat}
@@ -1318,6 +1324,7 @@ export default function PlaceFormModal(props: PlaceFormModalProps) {
phone={form.phone ?? ''}
email={form.email ?? ''}
openingHours={form.opening_hours ?? ''}
+ suggestedHours={S.detailsHours}
onChange={(field, value) => handleChange(field, value)}
/>
diff --git a/client/src/components/Planner/placeHours.test.ts b/client/src/components/Planner/placeHours.test.ts
index 26f19e821..15d50caf7 100644
--- a/client/src/components/Planner/placeHours.test.ts
+++ b/client/src/components/Planner/placeHours.test.ts
@@ -1,6 +1,6 @@
// FE-PLANNER-HOURS-001 to FE-PLANNER-HOURS-003
import { describe, expect, it } from 'vitest'
-import { emptyWeek, hoursLines, periodsFromWeek, readWeek, weekdayNames, writeWeek } from './placeHours'
+import { cleanWeek, emptyWeek, hoursLines, periodsFromWeek, readWeek, weekdayNames, weekFromPeriods, writeWeek } from './placeHours'
const week = JSON.stringify([
{ closed: false, open: '09:00', close: '17:00' },
@@ -37,3 +37,29 @@ describe('hand-kept opening hours (#2472)', () => {
expect(periodsFromWeek(null)).toEqual([])
})
})
+
+describe('typing and taking over (#2472)', () => {
+ it('FE-PLANNER-HOURS-004: half-typed times stay out of what is stored', () => {
+ const week = emptyWeek()
+ week[0] = { closed: false, open: '09:3', close: '17:00' }
+ expect(cleanWeek(week)[0]).toEqual({ closed: false, close: '17:00' })
+ expect(readWeek(writeWeek(cleanWeek(week)))[0]).toEqual({ closed: false, close: '17:00' })
+ })
+
+ it('FE-PLANNER-HOURS-005: looked-up periods become a week: one range a day, the rest closed, overnight and 24h handled', () => {
+ const p = (day: number, h: number, m = 0) => ({ day, hour: h, minute: m })
+ const week = weekFromPeriods([
+ { open: p(1, 9), close: p(1, 12) },
+ { open: p(1, 14), close: p(1, 18, 30) },
+ { open: p(5, 20), close: p(6, 2) },
+ { open: p(0, 0), close: null },
+ ])!
+ expect(week[0]).toEqual({ closed: false, open: '09:00', close: '18:30' })
+ expect(week[1]).toEqual({ closed: true })
+ expect(week[4]).toEqual({ closed: false, open: '20:00', close: '02:00' })
+ expect(week[6]).toEqual({ closed: false, open: '00:00', close: '23:59' })
+ expect(weekFromPeriods(null)).toBeNull()
+ expect(weekFromPeriods([])).toBeNull()
+ })
+})
+
diff --git a/client/src/components/Planner/placeHours.ts b/client/src/components/Planner/placeHours.ts
index 2a12f1df4..5760bb960 100644
--- a/client/src/components/Planner/placeHours.ts
+++ b/client/src/components/Planner/placeHours.ts
@@ -57,3 +57,45 @@ export function periodsFromWeek(raw: string | null | undefined): { open: { day:
return [{ open: point(day.open, googleDay), close: point(day.close, overnight ? (googleDay + 1) % 7 : googleDay) }]
})
}
+
+const HHMM = /^([01]\d|2[0-3]):[0-5]\d$/
+
+/**
+ * The week with only finished times in it. The time field reports every key
+ * press ("0", "09", "09:3"), and a half-typed time must neither be stored nor
+ * make the stored week unreadable; it stays in the editor's own draft.
+ */
+export function cleanWeek(week: PlaceOpeningHours): PlaceOpeningHours {
+ return week.map(day => ({
+ closed: day.closed,
+ ...(day.open && HHMM.test(day.open) ? { open: day.open } : {}),
+ ...(day.close && HHMM.test(day.close) ? { close: day.close } : {}),
+ }))
+}
+
+interface Point { day: number; hour: number; minute: number }
+
+/**
+ * Looked-up hours as a week for the editor (#2472): what the place details
+ * column shows, so a place edited later starts from them instead of from
+ * nothing. One range per day, the earliest opening to the latest closing, which
+ * is what the editor can hold; a day the provider names no period for is
+ * closed. A period that never closes is open around the clock. Null when the
+ * provider gave no periods at all.
+ */
+export function weekFromPeriods(periods: { open: Point; close?: Point | null }[] | null | undefined): PlaceOpeningHours | null {
+ if (!periods?.length) return null
+ // Minutes after the day's own midnight; a close past midnight counts on top of 24h.
+ const span: ({ open: number; close: number } | null)[] = Array.from({ length: PLACE_HOURS_DAYS }, () => null)
+ for (const period of periods) {
+ const i = (period.open.day + 6) % 7
+ const open = period.open.hour * 60 + period.open.minute
+ let close = period.close ? period.close.hour * 60 + period.close.minute : 24 * 60 - 1
+ if (period.close && close <= open) close += 24 * 60
+ const cur = span[i]
+ span[i] = cur ? { open: Math.min(cur.open, open), close: Math.max(cur.close, close) } : { open, close }
+ }
+ const clock = (m: number) => `${String(Math.floor(m / 60) % 24).padStart(2, '0')}:${String(m % 60).padStart(2, '0')}`
+ const week: PlaceOpeningHours = span.map(s => (s ? { closed: false, open: clock(s.open), close: clock(s.close) } : { closed: true }))
+ return week
+}
diff --git a/shared/src/i18n/ar/places.ts b/shared/src/i18n/ar/places.ts
index b575f7926..f02ee24d3 100644
--- a/shared/src/i18n/ar/places.ts
+++ b/shared/src/i18n/ar/places.ts
@@ -102,6 +102,7 @@ const places: TranslationStrings = {
'places.hoursCloses': 'يغلق',
'places.hoursCopyFirst': 'استخدم ساعات {day} لكل الأيام',
'places.hoursRemove': 'إزالة ساعات العمل',
+ 'places.hoursFromDetails': 'الأخذ من تفاصيل المكان',
'places.searchNothing': 'لم يتم العثور على شيء لـ «{query}»',
'places.searchNothingHint': 'أضفه بنفسك: للعنوان والهاتف والبريد وساعات العمل حقول بالأسفل.',
'places.addByHand': 'أضف يدويًا',
diff --git a/shared/src/i18n/az/places.ts b/shared/src/i18n/az/places.ts
index 8451d5c12..0aaef1e94 100644
--- a/shared/src/i18n/az/places.ts
+++ b/shared/src/i18n/az/places.ts
@@ -112,6 +112,7 @@ const places: TranslationStrings = {
'places.hoursCloses': 'bağlanır',
'places.hoursCopyFirst': '{day} saatlarını bütün günlərə tətbiq et',
'places.hoursRemove': 'İş saatlarını sil',
+ 'places.hoursFromDetails': 'Yer təfərrüatlarından götür',
'places.searchNothing': '“{query}” üçün heç nə tapılmadı',
'places.searchNothingHint': 'Özünüz əlavə edin: ünvan, telefon, e-poçt və iş saatları üçün aşağıda sahə var.',
'places.addByHand': 'Əllə əlavə et',
diff --git a/shared/src/i18n/br/places.ts b/shared/src/i18n/br/places.ts
index e02a1d365..264628d38 100644
--- a/shared/src/i18n/br/places.ts
+++ b/shared/src/i18n/br/places.ts
@@ -102,6 +102,7 @@ const places: TranslationStrings = {
'places.hoursCloses': 'fecha',
'places.hoursCopyFirst': 'Usar o horário de {day} em todos os dias',
'places.hoursRemove': 'Remover horário de funcionamento',
+ 'places.hoursFromDetails': 'Usar dos detalhes do lugar',
'places.searchNothing': 'Nada encontrado para “{query}”',
'places.searchNothingHint': 'Adicione você mesmo: endereço, telefone, e-mail e horário têm campos abaixo.',
'places.addByHand': 'Adicionar manualmente',
diff --git a/shared/src/i18n/ca/places.ts b/shared/src/i18n/ca/places.ts
index cdbd86e26..078ec5c50 100644
--- a/shared/src/i18n/ca/places.ts
+++ b/shared/src/i18n/ca/places.ts
@@ -101,6 +101,7 @@ const places: TranslationStrings = {
'places.hoursCloses': 'tanca',
'places.hoursCopyFirst': "Fes servir l'horari de {day} per a tots els dies",
'places.hoursRemove': "Elimina l'horari",
+ 'places.hoursFromDetails': 'Agafa-ho dels detalls del lloc',
'places.searchNothing': "No s'ha trobat res per a «{query}»",
'places.searchNothingHint': 'Afegeix-lo tu: adreça, telèfon, correu i horari tenen un camp a sota.',
'places.addByHand': 'Afegeix a mà',
diff --git a/shared/src/i18n/cs/places.ts b/shared/src/i18n/cs/places.ts
index f63dd506c..032c413a0 100644
--- a/shared/src/i18n/cs/places.ts
+++ b/shared/src/i18n/cs/places.ts
@@ -102,6 +102,7 @@ const places: TranslationStrings = {
'places.hoursCloses': 'zavírá',
'places.hoursCopyFirst': 'Použít časy z {day} pro všechny dny',
'places.hoursRemove': 'Odebrat otevírací dobu',
+ 'places.hoursFromDetails': 'Převzít z podrobností místa',
'places.searchNothing': 'Pro „{query}“ nebylo nic nalezeno',
'places.searchNothingHint': 'Přidejte ho sami: adresa, telefon, e-mail i otevírací doba mají pole níže.',
'places.addByHand': 'Přidat ručně',
diff --git a/shared/src/i18n/de/places.ts b/shared/src/i18n/de/places.ts
index 14523f5e2..5f1e62caf 100644
--- a/shared/src/i18n/de/places.ts
+++ b/shared/src/i18n/de/places.ts
@@ -102,6 +102,7 @@ const places: TranslationStrings = {
'places.hoursCloses': 'schließt',
'places.hoursCopyFirst': 'Zeiten vom {day} für alle Tage übernehmen',
'places.hoursRemove': 'Öffnungszeiten entfernen',
+ 'places.hoursFromDetails': 'Aus den Ortsdetails übernehmen',
'places.searchNothing': 'Nichts gefunden für „{query}“',
'places.searchNothingHint': 'Leg ihn selbst an: Adresse, Telefon, E-Mail und Öffnungszeiten haben unten ein Feld.',
'places.addByHand': 'Selbst anlegen',
diff --git a/shared/src/i18n/en/places.ts b/shared/src/i18n/en/places.ts
index f9c946e6d..bd483125a 100644
--- a/shared/src/i18n/en/places.ts
+++ b/shared/src/i18n/en/places.ts
@@ -106,6 +106,7 @@ const places: TranslationStrings = {
'places.hoursCloses': 'closes',
'places.hoursCopyFirst': "Use {day}'s hours for every day",
'places.hoursRemove': 'Remove opening hours',
+ 'places.hoursFromDetails': 'Take over from the place details',
'places.searchNothing': 'Nothing found for “{query}”',
'places.searchNothingHint': 'Add it yourself: address, phone, e-mail and opening hours all have a field below.',
'places.addByHand': 'Add by hand',
diff --git a/shared/src/i18n/es/places.ts b/shared/src/i18n/es/places.ts
index 387b53fce..750b76092 100644
--- a/shared/src/i18n/es/places.ts
+++ b/shared/src/i18n/es/places.ts
@@ -103,6 +103,7 @@ const places: TranslationStrings = {
'places.hoursCloses': 'cierra',
'places.hoursCopyFirst': 'Usar el horario del {day} para todos los días',
'places.hoursRemove': 'Quitar horario',
+ 'places.hoursFromDetails': 'Tomar de los detalles del lugar',
'places.searchNothing': 'No se encontró nada para «{query}»',
'places.searchNothingHint': 'Añádelo tú: dirección, teléfono, correo y horario tienen un campo abajo.',
'places.addByHand': 'Añadir a mano',
diff --git a/shared/src/i18n/et/places.ts b/shared/src/i18n/et/places.ts
index 56fcf8dd8..1d24bd472 100644
--- a/shared/src/i18n/et/places.ts
+++ b/shared/src/i18n/et/places.ts
@@ -103,6 +103,7 @@ const places: TranslationStrings = {
'places.hoursCloses': 'suletakse',
'places.hoursCopyFirst': 'Kasuta päeva {day} aegu kõigil päevadel',
'places.hoursRemove': 'Eemalda lahtiolekuajad',
+ 'places.hoursFromDetails': 'Võta üle koha andmetest',
'places.searchNothing': 'Otsingule „{query}“ ei leitud midagi',
'places.searchNothingHint': 'Lisa see ise: aadressil, telefonil, e-postil ja lahtiolekuaegadel on all väli.',
'places.addByHand': 'Lisa käsitsi',
diff --git a/shared/src/i18n/fr/places.ts b/shared/src/i18n/fr/places.ts
index a97b7eb59..80730f335 100644
--- a/shared/src/i18n/fr/places.ts
+++ b/shared/src/i18n/fr/places.ts
@@ -103,6 +103,7 @@ const places: TranslationStrings = {
'places.hoursCloses': 'ferme',
'places.hoursCopyFirst': 'Appliquer les horaires du {day} à tous les jours',
'places.hoursRemove': 'Supprimer les horaires',
+ 'places.hoursFromDetails': 'Reprendre depuis les détails du lieu',
'places.searchNothing': 'Aucun résultat pour « {query} »',
'places.searchNothingHint': 'Ajoutez-le vous-même : adresse, téléphone, e-mail et horaires ont un champ plus bas.',
'places.addByHand': 'Ajouter à la main',
diff --git a/shared/src/i18n/gr/places.ts b/shared/src/i18n/gr/places.ts
index fd6ff861a..10ca96197 100644
--- a/shared/src/i18n/gr/places.ts
+++ b/shared/src/i18n/gr/places.ts
@@ -103,6 +103,7 @@ const places: TranslationStrings = {
'places.hoursCloses': 'κλείνει',
'places.hoursCopyFirst': 'Χρήση του ωραρίου της ημέρας {day} για όλες τις ημέρες',
'places.hoursRemove': 'Αφαίρεση ωραρίου',
+ 'places.hoursFromDetails': 'Λήψη από τις λεπτομέρειες τοποθεσίας',
'places.searchNothing': 'Δεν βρέθηκε τίποτα για «{query}»',
'places.searchNothingHint': 'Προσθέστε το μόνοι σας: διεύθυνση, τηλέφωνο, email και ωράριο έχουν πεδίο παρακάτω.',
'places.addByHand': 'Χειροκίνητη προσθήκη',
diff --git a/shared/src/i18n/hu/places.ts b/shared/src/i18n/hu/places.ts
index daf1d56f0..42f3017cc 100644
--- a/shared/src/i18n/hu/places.ts
+++ b/shared/src/i18n/hu/places.ts
@@ -102,6 +102,7 @@ const places: TranslationStrings = {
'places.hoursCloses': 'zár',
'places.hoursCopyFirst': 'A(z) {day} nyitvatartása minden napra',
'places.hoursRemove': 'Nyitvatartás törlése',
+ 'places.hoursFromDetails': 'Átvétel a hely adataiból',
'places.searchNothing': 'Nincs találat erre: „{query}”',
'places.searchNothingHint': 'Add hozzá magad: a cím, telefon, e-mail és nyitvatartás lent kitölthető.',
'places.addByHand': 'Kézi hozzáadás',
diff --git a/shared/src/i18n/id/places.ts b/shared/src/i18n/id/places.ts
index d47b5555e..db53829ab 100644
--- a/shared/src/i18n/id/places.ts
+++ b/shared/src/i18n/id/places.ts
@@ -101,6 +101,7 @@ const places: TranslationStrings = {
'places.hoursCloses': 'tutup',
'places.hoursCopyFirst': 'Pakai jam {day} untuk semua hari',
'places.hoursRemove': 'Hapus jam buka',
+ 'places.hoursFromDetails': 'Ambil dari detail tempat',
'places.searchNothing': 'Tidak ada hasil untuk “{query}”',
'places.searchNothingHint': 'Tambahkan sendiri: alamat, telepon, email, dan jam buka ada di bawah.',
'places.addByHand': 'Tambah manual',
diff --git a/shared/src/i18n/it/places.ts b/shared/src/i18n/it/places.ts
index d143bd9c6..e69726f88 100644
--- a/shared/src/i18n/it/places.ts
+++ b/shared/src/i18n/it/places.ts
@@ -102,6 +102,7 @@ const places: TranslationStrings = {
'places.hoursCloses': 'chiude',
'places.hoursCopyFirst': 'Usa gli orari di {day} per tutti i giorni',
'places.hoursRemove': 'Rimuovi orari di apertura',
+ 'places.hoursFromDetails': 'Prendi dai dettagli del luogo',
'places.searchNothing': 'Nessun risultato per «{query}»',
'places.searchNothingHint': 'Aggiungilo tu: indirizzo, telefono, e-mail e orari hanno un campo qui sotto.',
'places.addByHand': 'Aggiungi a mano',
diff --git a/shared/src/i18n/ja/places.ts b/shared/src/i18n/ja/places.ts
index bae017fe7..711a9f85e 100644
--- a/shared/src/i18n/ja/places.ts
+++ b/shared/src/i18n/ja/places.ts
@@ -102,6 +102,7 @@ const places: TranslationStrings = {
'places.hoursCloses': '閉店',
'places.hoursCopyFirst': '{day}の時間を全曜日に適用',
'places.hoursRemove': '営業時間を削除',
+ 'places.hoursFromDetails': '場所の詳細から取り込む',
'places.searchNothing': '「{query}」は見つかりませんでした',
'places.searchNothingHint': '自分で追加できます。住所・電話・メール・営業時間の欄が下にあります。',
'places.addByHand': '手動で追加',
diff --git a/shared/src/i18n/ko/places.ts b/shared/src/i18n/ko/places.ts
index 2c693d5b4..1373a7524 100644
--- a/shared/src/i18n/ko/places.ts
+++ b/shared/src/i18n/ko/places.ts
@@ -101,6 +101,7 @@ const places: TranslationStrings = {
'places.hoursCloses': '마감',
'places.hoursCopyFirst': '{day} 시간을 모든 요일에 적용',
'places.hoursRemove': '영업시간 삭제',
+ 'places.hoursFromDetails': '장소 정보에서 가져오기',
'places.searchNothing': '“{query}”에 대한 결과가 없습니다',
'places.searchNothingHint': '직접 추가하세요. 주소, 전화, 이메일, 영업시간 입력란이 아래에 있습니다.',
'places.addByHand': '직접 추가',
diff --git a/shared/src/i18n/nl/places.ts b/shared/src/i18n/nl/places.ts
index fdaca08a2..7ba46c47f 100644
--- a/shared/src/i18n/nl/places.ts
+++ b/shared/src/i18n/nl/places.ts
@@ -102,6 +102,7 @@ const places: TranslationStrings = {
'places.hoursCloses': 'sluit',
'places.hoursCopyFirst': 'Tijden van {day} voor alle dagen gebruiken',
'places.hoursRemove': 'Openingstijden verwijderen',
+ 'places.hoursFromDetails': 'Overnemen uit de plekdetails',
'places.searchNothing': 'Niets gevonden voor ‘{query}’',
'places.searchNothingHint': 'Voeg het zelf toe: adres, telefoon, e-mail en openingstijden hebben hieronder een veld.',
'places.addByHand': 'Zelf toevoegen',
diff --git a/shared/src/i18n/pl/places.ts b/shared/src/i18n/pl/places.ts
index dcbd179f8..eb721e9c0 100644
--- a/shared/src/i18n/pl/places.ts
+++ b/shared/src/i18n/pl/places.ts
@@ -92,6 +92,7 @@ const places: TranslationStrings = {
'places.hoursCloses': 'zamknięcie',
'places.hoursCopyFirst': 'Użyj godzin z dnia {day} dla wszystkich dni',
'places.hoursRemove': 'Usuń godziny otwarcia',
+ 'places.hoursFromDetails': 'Przejmij ze szczegółów miejsca',
'places.searchNothing': 'Nic nie znaleziono dla „{query}”',
'places.searchNothingHint': 'Dodaj samodzielnie: adres, telefon, e-mail i godziny otwarcia mają pola poniżej.',
'places.addByHand': 'Dodaj ręcznie',
diff --git a/shared/src/i18n/ru/places.ts b/shared/src/i18n/ru/places.ts
index e60f5a9b3..fae8ba678 100644
--- a/shared/src/i18n/ru/places.ts
+++ b/shared/src/i18n/ru/places.ts
@@ -102,6 +102,7 @@ const places: TranslationStrings = {
'places.hoursCloses': 'закрывается',
'places.hoursCopyFirst': 'Применить часы дня «{day}» ко всем дням',
'places.hoursRemove': 'Удалить часы работы',
+ 'places.hoursFromDetails': 'Взять из сведений о месте',
'places.searchNothing': 'По запросу «{query}» ничего не найдено',
'places.searchNothingHint': 'Добавьте сами: для адреса, телефона, почты и часов работы есть поля ниже.',
'places.addByHand': 'Добавить вручную',
diff --git a/shared/src/i18n/sk/places.ts b/shared/src/i18n/sk/places.ts
index ff86220c9..21f7e7d81 100644
--- a/shared/src/i18n/sk/places.ts
+++ b/shared/src/i18n/sk/places.ts
@@ -103,6 +103,7 @@ const places: TranslationStrings = {
'places.hoursCloses': 'zatvára',
'places.hoursCopyFirst': 'Použiť časy z {day} pre všetky dni',
'places.hoursRemove': 'Odstrániť otváracie hodiny',
+ 'places.hoursFromDetails': 'Prevziať z podrobností miesta',
'places.searchNothing': 'Pre „{query}“ sa nič nenašlo',
'places.searchNothingHint': 'Pridajte ho sami: adresa, telefón, e-mail aj otváracie hodiny majú pole nižšie.',
'places.addByHand': 'Pridať ručne',
diff --git a/shared/src/i18n/sv/places.ts b/shared/src/i18n/sv/places.ts
index b38207642..86675fae1 100644
--- a/shared/src/i18n/sv/places.ts
+++ b/shared/src/i18n/sv/places.ts
@@ -102,6 +102,7 @@ const places: TranslationStrings = {
'places.hoursCloses': 'stänger',
'places.hoursCopyFirst': 'Använd tiderna för {day} alla dagar',
'places.hoursRemove': 'Ta bort öppettider',
+ 'places.hoursFromDetails': 'Hämta från platsdetaljerna',
'places.searchNothing': 'Inget hittades för ”{query}”',
'places.searchNothingHint': 'Lägg till den själv: adress, telefon, e-post och öppettider har fält nedanför.',
'places.addByHand': 'Lägg till själv',
diff --git a/shared/src/i18n/th/places.ts b/shared/src/i18n/th/places.ts
index 55636405f..46beee441 100644
--- a/shared/src/i18n/th/places.ts
+++ b/shared/src/i18n/th/places.ts
@@ -96,6 +96,7 @@ const places: TranslationStrings = {
'places.hoursCloses': 'ปิด',
'places.hoursCopyFirst': 'ใช้เวลาของวัน{day}กับทุกวัน',
'places.hoursRemove': 'ลบเวลาทำการ',
+ 'places.hoursFromDetails': 'ดึงจากรายละเอียดสถานที่',
'places.searchNothing': 'ไม่พบผลลัพธ์สำหรับ “{query}”',
'places.searchNothingHint': 'เพิ่มเองได้: ที่อยู่ โทรศัพท์ อีเมล และเวลาทำการมีช่องให้กรอกด้านล่าง',
'places.addByHand': 'เพิ่มเอง',
diff --git a/shared/src/i18n/tr/places.ts b/shared/src/i18n/tr/places.ts
index 32b0d57ba..a8c0ea971 100644
--- a/shared/src/i18n/tr/places.ts
+++ b/shared/src/i18n/tr/places.ts
@@ -104,6 +104,7 @@ const places: TranslationStrings = {
'places.hoursCloses': 'kapanış',
'places.hoursCopyFirst': '{day} saatlerini tüm günlere uygula',
'places.hoursRemove': 'Çalışma saatlerini kaldır',
+ 'places.hoursFromDetails': 'Yer ayrıntılarından al',
'places.searchNothing': '“{query}” için sonuç bulunamadı',
'places.searchNothingHint': 'Kendin ekle: adres, telefon, e-posta ve çalışma saatleri için aşağıda alan var.',
'places.addByHand': 'Elle ekle',
diff --git a/shared/src/i18n/uk/places.ts b/shared/src/i18n/uk/places.ts
index b9059bd9c..df379f2d9 100644
--- a/shared/src/i18n/uk/places.ts
+++ b/shared/src/i18n/uk/places.ts
@@ -102,6 +102,7 @@ const places: TranslationStrings = {
'places.hoursCloses': 'зачиняється',
'places.hoursCopyFirst': 'Застосувати години дня «{day}» до всіх днів',
'places.hoursRemove': 'Видалити години роботи',
+ 'places.hoursFromDetails': 'Взяти з даних про місце',
'places.searchNothing': 'За запитом «{query}» нічого не знайдено',
'places.searchNothingHint': 'Додайте самі: для адреси, телефону, пошти й годин роботи є поля нижче.',
'places.addByHand': 'Додати вручну',
diff --git a/shared/src/i18n/vi/places.ts b/shared/src/i18n/vi/places.ts
index 54bed1fce..b3e5e6293 100644
--- a/shared/src/i18n/vi/places.ts
+++ b/shared/src/i18n/vi/places.ts
@@ -102,6 +102,7 @@ const places: TranslationStrings = {
'places.hoursCloses': 'đóng cửa',
'places.hoursCopyFirst': 'Dùng giờ của {day} cho mọi ngày',
'places.hoursRemove': 'Xóa giờ mở cửa',
+ 'places.hoursFromDetails': 'Lấy từ thông tin địa điểm',
'places.searchNothing': 'Không tìm thấy “{query}”',
'places.searchNothingHint': 'Tự thêm: địa chỉ, điện thoại, email và giờ mở cửa đều có ô bên dưới.',
'places.addByHand': 'Thêm thủ công',
diff --git a/shared/src/i18n/zh-TW/places.ts b/shared/src/i18n/zh-TW/places.ts
index a6e95000e..fd25d62b2 100644
--- a/shared/src/i18n/zh-TW/places.ts
+++ b/shared/src/i18n/zh-TW/places.ts
@@ -101,6 +101,7 @@ const places: TranslationStrings = {
'places.hoursCloses': '關門',
'places.hoursCopyFirst': '將{day}的時間套用到每一天',
'places.hoursRemove': '移除營業時間',
+ 'places.hoursFromDetails': '從地點詳情帶入',
'places.searchNothing': '找不到「{query}」',
'places.searchNothingHint': '自行新增:下方有地址、電話、電子郵件和營業時間的欄位。',
'places.addByHand': '手動新增',
diff --git a/shared/src/i18n/zh/places.ts b/shared/src/i18n/zh/places.ts
index 4d822c7fc..a5a78d658 100644
--- a/shared/src/i18n/zh/places.ts
+++ b/shared/src/i18n/zh/places.ts
@@ -101,6 +101,7 @@ const places: TranslationStrings = {
'places.hoursCloses': '关门',
'places.hoursCopyFirst': '将{day}的时间用于每一天',
'places.hoursRemove': '移除营业时间',
+ 'places.hoursFromDetails': '从地点详情获取',
'places.searchNothing': '未找到“{query}”',
'places.searchNothingHint': '自己添加:下方有地址、电话、电子邮件和营业时间的输入框。',
'places.addByHand': '手动添加',