Add an admin switch for place enrichment

Reads fail-open, unlike the three older places_* switches. Those read
=== 'true', which is why migration 185 had to backfill a row on every existing
install; there is nothing here that warrants repeating that.

The three hand-rolled toggles next to it move to ToggleSwitch while I am in
there - they were bare buttons with an inline transform and no aria-pressed or
label, so a screen reader could not tell their state.
This commit is contained in:
Maurice
2026-08-09 22:31:39 +02:00
committed by Maurice
parent e9f9cfb5d5
commit f2368f86cf
57 changed files with 437 additions and 24 deletions
+3 -2
View File
@@ -261,7 +261,7 @@ function RouteFallback() {
} }
export default function App() { export default function App() {
const { loadUser, isAuthenticated, demoMode, setDemoMode, setDevMode, setIsPrerelease, setAppVersion, setHasMapsKey, setServerTimezone, setAppRequireMfa, setTripRemindersEnabled, setPlacesPhotosEnabled, setPlacesAutocompleteEnabled, setPlacesDetailsEnabled } = useAuthStore() const { loadUser, isAuthenticated, demoMode, setDemoMode, setDevMode, setIsPrerelease, setAppVersion, setHasMapsKey, setServerTimezone, setAppRequireMfa, setTripRemindersEnabled, setPlacesPhotosEnabled, setPlacesAutocompleteEnabled, setPlacesDetailsEnabled, setPlacesEnrichEnabled } = useAuthStore()
const { loadSettings } = useSettingsStore() const { loadSettings } = useSettingsStore()
const { loadAddons } = useAddonStore() const { loadAddons } = useAddonStore()
const { loadPlugins } = usePluginStore() const { loadPlugins } = usePluginStore()
@@ -278,7 +278,7 @@ export default function App() {
loadUser() loadUser()
} }
} }
authApi.getAppConfig().then(async (config: { demo_mode?: boolean; dev_mode?: boolean; is_prerelease?: boolean; has_maps_key?: boolean; version?: string; timezone?: string; require_mfa?: boolean; trip_reminders_enabled?: boolean; places_photos_enabled?: boolean; places_autocomplete_enabled?: boolean; places_details_enabled?: boolean; permissions?: Record<string, PermissionLevel> }) => { authApi.getAppConfig().then(async (config: { demo_mode?: boolean; dev_mode?: boolean; is_prerelease?: boolean; has_maps_key?: boolean; version?: string; timezone?: string; require_mfa?: boolean; trip_reminders_enabled?: boolean; places_photos_enabled?: boolean; places_autocomplete_enabled?: boolean; places_details_enabled?: boolean; places_enrich_enabled?: boolean; permissions?: Record<string, PermissionLevel> }) => {
setDemoMode(!!config?.demo_mode) setDemoMode(!!config?.demo_mode)
if (config?.dev_mode) setDevMode(true) if (config?.dev_mode) setDevMode(true)
if (config?.is_prerelease !== undefined) setIsPrerelease(config.is_prerelease) if (config?.is_prerelease !== undefined) setIsPrerelease(config.is_prerelease)
@@ -290,6 +290,7 @@ export default function App() {
if (config?.places_photos_enabled !== undefined) setPlacesPhotosEnabled(config.places_photos_enabled) if (config?.places_photos_enabled !== undefined) setPlacesPhotosEnabled(config.places_photos_enabled)
if (config?.places_autocomplete_enabled !== undefined) setPlacesAutocompleteEnabled(config.places_autocomplete_enabled) if (config?.places_autocomplete_enabled !== undefined) setPlacesAutocompleteEnabled(config.places_autocomplete_enabled)
if (config?.places_details_enabled !== undefined) setPlacesDetailsEnabled(config.places_details_enabled) if (config?.places_details_enabled !== undefined) setPlacesDetailsEnabled(config.places_details_enabled)
if (config?.places_enrich_enabled !== undefined) setPlacesEnrichEnabled(config.places_enrich_enabled)
if (config?.permissions) usePermissionsStore.getState().setPermissions(config.permissions) if (config?.permissions) usePermissionsStore.getState().setPermissions(config.permissions)
if (config?.version) { if (config?.version) {
+14
View File
@@ -8,6 +8,7 @@ import {
channelTestResultSchema, channelTestResultSchema,
mapsSearchResultSchema, mapsAutocompleteResultSchema, mapsPlaceDetailsResultSchema, mapsSearchResultSchema, mapsAutocompleteResultSchema, mapsPlaceDetailsResultSchema,
mapsPlacePhotoResultSchema, mapsReverseResultSchema, mapsResolveUrlResultSchema, mapsPlacePhotoResultSchema, mapsReverseResultSchema, mapsResolveUrlResultSchema,
mapsPlaceEnrichmentResultSchema,
type NotificationRespondRequest, type NotificationRespondRequest,
type SettingUpsertRequest, type SettingsBulkRequest, type SettingUpsertRequest, type SettingsBulkRequest,
type JourneyCreateRequest, type JourneyAddTripRequest, type JourneyCreateRequest, type JourneyAddTripRequest,
@@ -599,6 +600,8 @@ export const adminApi = {
updatePlacesAutocomplete: (enabled: boolean) => apiClient.put('/admin/places-autocomplete', { enabled }).then(r => r.data), updatePlacesAutocomplete: (enabled: boolean) => apiClient.put('/admin/places-autocomplete', { enabled }).then(r => r.data),
getPlacesDetails: () => apiClient.get('/admin/places-details').then(r => r.data), getPlacesDetails: () => apiClient.get('/admin/places-details').then(r => r.data),
updatePlacesDetails: (enabled: boolean) => apiClient.put('/admin/places-details', { enabled }).then(r => r.data), updatePlacesDetails: (enabled: boolean) => apiClient.put('/admin/places-details', { enabled }).then(r => r.data),
getPlacesEnrich: () => apiClient.get('/admin/places-enrich').then(r => r.data),
updatePlacesEnrich: (enabled: boolean) => apiClient.put('/admin/places-enrich', { enabled }).then(r => r.data),
getCollabFeatures: () => apiClient.get('/admin/collab-features').then(r => r.data), getCollabFeatures: () => apiClient.get('/admin/collab-features').then(r => r.data),
updateCollabFeatures: (features: Record<string, boolean>) => apiClient.put('/admin/collab-features', features).then(r => r.data), updateCollabFeatures: (features: Record<string, boolean>) => apiClient.put('/admin/collab-features', features).then(r => r.data),
packingTemplates: () => apiClient.get('/admin/packing-templates').then(r => r.data), packingTemplates: () => apiClient.get('/admin/packing-templates').then(r => r.data),
@@ -926,6 +929,17 @@ export const mapsApi = {
autocomplete: (input: string, lang?: string, locationBias?: { low: { lat: number; lng: number }; high: { lat: number; lng: number } }, signal?: AbortSignal) => autocomplete: (input: string, lang?: string, locationBias?: { low: { lat: number; lng: number }; high: { lat: number; lng: number } }, signal?: AbortSignal) =>
apiClient.post('/maps/autocomplete', { input, lang, locationBias }, { signal }).then(r => checkInDev(mapsAutocompleteResultSchema, r.data, 'maps.autocomplete')), apiClient.post('/maps/autocomplete', { input, lang, locationBias }, { signal }).then(r => checkInDev(mapsAutocompleteResultSchema, r.data, 'maps.autocomplete')),
details: (placeId: string, lang?: string) => apiClient.get(`/maps/details/${encodeURIComponent(placeId)}`, { params: { lang } }).then(r => checkInDev(mapsPlaceDetailsResultSchema, r.data, 'maps.details')), details: (placeId: string, lang?: string) => apiClient.get(`/maps/details/${encodeURIComponent(placeId)}`, { params: { lang } }).then(r => checkInDev(mapsPlaceDetailsResultSchema, r.data, 'maps.details')),
// Pictures and a description for a place that is being looked at but not yet
// saved. Fans out to several providers server-side, so it takes a signal and
// the caller is expected to abort it when the selection changes.
placeEnrichment: (
body: { placeId?: string; lat: number; lng: number; name: string; lang?: string },
signal?: AbortSignal,
) => apiClient.post('/maps/enrichment', body, { signal }).then(r => checkInDev(mapsPlaceEnrichmentResultSchema, r.data, 'maps.placeEnrichment')),
// Author + licence for a picture already stored in the photo cache, keyed by
// the cache key embedded in its proxy URL. Local read, no provider call.
placePhotoCredit: (key: string) =>
apiClient.get(`/maps/enrichment/credit/${encodeURIComponent(key)}`).then(r => r.data as { credit: string | null }),
placePhoto: (placeId: string, lat?: number, lng?: number, name?: string) => apiClient.get(`/maps/place-photo/${encodeURIComponent(placeId)}`, { params: { lat, lng, name } }).then(r => checkInDev(mapsPlacePhotoResultSchema, r.data, 'maps.placePhoto')), placePhoto: (placeId: string, lat?: number, lng?: number, name?: string) => apiClient.get(`/maps/place-photo/${encodeURIComponent(placeId)}`, { params: { lat, lng, name } }).then(r => checkInDev(mapsPlacePhotoResultSchema, r.data, 'maps.placePhoto')),
reverse: (lat: number, lng: number, lang?: string) => apiClient.get('/maps/reverse', { params: { lat, lng, lang } }).then(r => checkInDev(mapsReverseResultSchema, r.data, 'maps.reverse')), reverse: (lat: number, lng: number, lang?: string) => apiClient.get('/maps/reverse', { params: { lat, lng, lang } }).then(r => checkInDev(mapsReverseResultSchema, r.data, 'maps.reverse')),
resolveUrl: (url: string) => apiClient.post('/maps/resolve-url', { url }).then(r => checkInDev(mapsResolveUrlResultSchema, r.data, 'maps.resolveUrl')), resolveUrl: (url: string) => apiClient.post('/maps/resolve-url', { url }).then(r => checkInDev(mapsResolveUrlResultSchema, r.data, 'maps.resolveUrl')),
@@ -25,10 +25,11 @@ interface MAdminSettingsSectionProps {
export default function MAdminSettingsSection({ admin, t }: MAdminSettingsSectionProps) { export default function MAdminSettingsSection({ admin, t }: MAdminSettingsSectionProps) {
const { const {
toast, toast,
setPlacesPhotosEnabled, setPlacesAutocompleteEnabled, setPlacesDetailsEnabled, setPlacesPhotosEnabled, setPlacesAutocompleteEnabled, setPlacesDetailsEnabled, setPlacesEnrichEnabled,
placesPhotosEnabled, setPlacesPhotosEnabledState, placesPhotosEnabled, setPlacesPhotosEnabledState,
placesAutocompleteEnabled, setPlacesAutocompleteEnabledState, placesAutocompleteEnabled, setPlacesAutocompleteEnabledState,
placesDetailsEnabled, setPlacesDetailsEnabledState, placesDetailsEnabled, setPlacesDetailsEnabledState,
placesEnrichEnabled, setPlacesEnrichEnabledState,
oidcConfig, setOidcConfig, savingOidc, setSavingOidc, oidcConfig, setOidcConfig, savingOidc, setSavingOidc,
passwordLogin, setPasswordLogin, passwordRegistration, setPasswordRegistration, passwordLogin, setPasswordLogin, passwordRegistration, setPasswordRegistration,
oidcLogin, setOidcLogin, oidcRegistration, setOidcRegistration, oidcLogin, setOidcLogin, oidcRegistration, setOidcRegistration,
@@ -323,6 +324,26 @@ export default function MAdminSettingsSection({ admin, t }: MAdminSettingsSectio
/> />
} }
/> />
<MAdminRow
title={t('admin.placesEnrich.title')}
hint={t('admin.placesEnrich.subtitle')}
trailing={
<MToggle
checked={placesEnrichEnabled}
ariaLabel={t('admin.placesEnrich.title')}
onChange={async (next) => {
setPlacesEnrichEnabledState(next)
setPlacesEnrichEnabled(next)
try {
await adminApi.updatePlacesEnrich(next)
} catch {
setPlacesEnrichEnabledState(!next)
setPlacesEnrichEnabled(!next)
}
}}
/>
}
/>
</div> </div>
{/* Open-Meteo weather info */} {/* Open-Meteo weather info */}
@@ -353,6 +353,27 @@ describe('AdminSettingsTab', () => {
expect(admin.setPlacesDetailsEnabled).toHaveBeenLastCalledWith(true); expect(admin.setPlacesDetailsEnabled).toHaveBeenLastCalledWith(true);
}); });
it('FE-ADMSET-030b: places enrichment toggle updates state and rolls back on failure', async () => {
server.use(http.put('/api/admin/places-enrich', () => HttpResponse.json({}, { status: 500 })));
const admin = renderTab({ placesEnrichEnabled: true });
fireEvent.click(toggleFor('Place Enrichment'));
expect(admin.setPlacesEnrichEnabledState).toHaveBeenNthCalledWith(1, false);
await waitFor(() => expect(admin.setPlacesEnrichEnabledState).toHaveBeenLastCalledWith(true));
expect(admin.setPlacesEnrichEnabled).toHaveBeenLastCalledWith(true);
});
it('FE-ADMSET-030c: the four places toggles expose their label to screen readers', () => {
// They moved from hand-rolled buttons to ToggleSwitch, which is what carries
// the aria-label and aria-pressed the bare markup never had.
renderTab({ placesPhotosEnabled: true, placesEnrichEnabled: false });
expect(toggleFor('Place Photos')).toHaveAttribute('aria-pressed', 'true');
expect(toggleFor('Place Enrichment')).toHaveAttribute('aria-pressed', 'false');
expect(toggleFor('Place Enrichment')).toHaveAttribute('aria-label', 'Place Enrichment');
});
it('FE-ADMSET-031: shows the Open-Meteo info block', () => { it('FE-ADMSET-031: shows the Open-Meteo info block', () => {
renderTab(); renderTab();
+36 -19
View File
@@ -2,6 +2,7 @@ import React from 'react'
import { adminApi, authApi } from '../../api/client' import { adminApi, authApi } from '../../api/client'
import { getApiErrorMessage } from '../../types' import { getApiErrorMessage } from '../../types'
import { Eye, EyeOff, Save, CheckCircle, XCircle, Loader2, Sun, RefreshCw, AlertTriangle } from 'lucide-react' import { Eye, EyeOff, Save, CheckCircle, XCircle, Loader2, Sun, RefreshCw, AlertTriangle } from 'lucide-react'
import ToggleSwitch from '../../components/Settings/ToggleSwitch'
import type { TranslationFn } from '../../types' import type { TranslationFn } from '../../types'
import type { useAdmin } from './useAdmin' import type { useAdmin } from './useAdmin'
@@ -15,10 +16,11 @@ interface AdminSettingsTabProps {
export default function AdminSettingsTab({ admin, t }: AdminSettingsTabProps): React.ReactElement { export default function AdminSettingsTab({ admin, t }: AdminSettingsTabProps): React.ReactElement {
const { const {
toast, toast,
setPlacesPhotosEnabled, setPlacesAutocompleteEnabled, setPlacesDetailsEnabled, setPlacesPhotosEnabled, setPlacesAutocompleteEnabled, setPlacesDetailsEnabled, setPlacesEnrichEnabled,
placesPhotosEnabled, setPlacesPhotosEnabledState, placesPhotosEnabled, setPlacesPhotosEnabledState,
placesAutocompleteEnabled, setPlacesAutocompleteEnabledState, placesAutocompleteEnabled, setPlacesAutocompleteEnabledState,
placesDetailsEnabled, setPlacesDetailsEnabledState, placesDetailsEnabled, setPlacesDetailsEnabledState,
placesEnrichEnabled, setPlacesEnrichEnabledState,
oidcConfig, setOidcConfig, savingOidc, setSavingOidc, oidcConfig, setOidcConfig, savingOidc, setSavingOidc,
passwordLogin, setPasswordLogin, passwordRegistration, setPasswordRegistration, passwordLogin, setPasswordLogin, passwordRegistration, setPasswordRegistration,
oidcLogin, setOidcLogin, oidcRegistration, setOidcRegistration, oidcLogin, setOidcLogin, oidcRegistration, setOidcRegistration,
@@ -334,17 +336,16 @@ export default function AdminSettingsTab({ admin, t }: AdminSettingsTabProps): R
<p className="text-sm font-medium text-slate-700">{t('admin.placesPhotos.title')}</p> <p className="text-sm font-medium text-slate-700">{t('admin.placesPhotos.title')}</p>
<p className="text-xs text-slate-400 mt-0.5">{t('admin.placesPhotos.subtitle')}</p> <p className="text-xs text-slate-400 mt-0.5">{t('admin.placesPhotos.subtitle')}</p>
</div> </div>
<button <ToggleSwitch
onClick={async () => { on={placesPhotosEnabled}
label={t('admin.placesPhotos.title')}
onToggle={async () => {
const next = !placesPhotosEnabled const next = !placesPhotosEnabled
setPlacesPhotosEnabledState(next) setPlacesPhotosEnabledState(next)
setPlacesPhotosEnabled(next) setPlacesPhotosEnabled(next)
try { await adminApi.updatePlacesPhotos(next) } catch { setPlacesPhotosEnabledState(!next); setPlacesPhotosEnabled(!next) } try { await adminApi.updatePlacesPhotos(next) } catch { setPlacesPhotosEnabledState(!next); setPlacesPhotosEnabled(!next) }
}} }}
className={`relative inline-flex h-6 w-11 flex-shrink-0 items-center rounded-full transition-colors ${placesPhotosEnabled ? 'bg-content' : 'bg-edge'}`} />
>
<span className="absolute left-0.5 h-5 w-5 rounded-full bg-white transition-transform duration-200" style={{ transform: placesPhotosEnabled ? 'translateX(20px)' : 'translateX(0)' }} />
</button>
</div> </div>
{/* Place Autocomplete Toggle */} {/* Place Autocomplete Toggle */}
@@ -353,17 +354,16 @@ export default function AdminSettingsTab({ admin, t }: AdminSettingsTabProps): R
<p className="text-sm font-medium text-slate-700">{t('admin.placesAutocomplete.title')}</p> <p className="text-sm font-medium text-slate-700">{t('admin.placesAutocomplete.title')}</p>
<p className="text-xs text-slate-400 mt-0.5">{t('admin.placesAutocomplete.subtitle')}</p> <p className="text-xs text-slate-400 mt-0.5">{t('admin.placesAutocomplete.subtitle')}</p>
</div> </div>
<button <ToggleSwitch
onClick={async () => { on={placesAutocompleteEnabled}
label={t('admin.placesAutocomplete.title')}
onToggle={async () => {
const next = !placesAutocompleteEnabled const next = !placesAutocompleteEnabled
setPlacesAutocompleteEnabledState(next) setPlacesAutocompleteEnabledState(next)
setPlacesAutocompleteEnabled(next) setPlacesAutocompleteEnabled(next)
try { await adminApi.updatePlacesAutocomplete(next) } catch { setPlacesAutocompleteEnabledState(!next); setPlacesAutocompleteEnabled(!next) } try { await adminApi.updatePlacesAutocomplete(next) } catch { setPlacesAutocompleteEnabledState(!next); setPlacesAutocompleteEnabled(!next) }
}} }}
className={`relative inline-flex h-6 w-11 flex-shrink-0 items-center rounded-full transition-colors ${placesAutocompleteEnabled ? 'bg-content' : 'bg-edge'}`} />
>
<span className="absolute left-0.5 h-5 w-5 rounded-full bg-white transition-transform duration-200" style={{ transform: placesAutocompleteEnabled ? 'translateX(20px)' : 'translateX(0)' }} />
</button>
</div> </div>
{/* Place Details Toggle */} {/* Place Details Toggle */}
@@ -372,17 +372,34 @@ export default function AdminSettingsTab({ admin, t }: AdminSettingsTabProps): R
<p className="text-sm font-medium text-slate-700">{t('admin.placesDetails.title')}</p> <p className="text-sm font-medium text-slate-700">{t('admin.placesDetails.title')}</p>
<p className="text-xs text-slate-400 mt-0.5">{t('admin.placesDetails.subtitle')}</p> <p className="text-xs text-slate-400 mt-0.5">{t('admin.placesDetails.subtitle')}</p>
</div> </div>
<button <ToggleSwitch
onClick={async () => { on={placesDetailsEnabled}
label={t('admin.placesDetails.title')}
onToggle={async () => {
const next = !placesDetailsEnabled const next = !placesDetailsEnabled
setPlacesDetailsEnabledState(next) setPlacesDetailsEnabledState(next)
setPlacesDetailsEnabled(next) setPlacesDetailsEnabled(next)
try { await adminApi.updatePlacesDetails(next) } catch { setPlacesDetailsEnabledState(!next); setPlacesDetailsEnabled(!next) } try { await adminApi.updatePlacesDetails(next) } catch { setPlacesDetailsEnabledState(!next); setPlacesDetailsEnabled(!next) }
}} }}
className={`relative inline-flex h-6 w-11 flex-shrink-0 items-center rounded-full transition-colors ${placesDetailsEnabled ? 'bg-content' : 'bg-edge'}`} />
> </div>
<span className="absolute left-0.5 h-5 w-5 rounded-full bg-white transition-transform duration-200" style={{ transform: placesDetailsEnabled ? 'translateX(20px)' : 'translateX(0)' }} />
</button> {/* Place Enrichment Toggle */}
<div className="flex items-center justify-between gap-4 py-3 border-t border-slate-100">
<div>
<p className="text-sm font-medium text-slate-700">{t('admin.placesEnrich.title')}</p>
<p className="text-xs text-slate-400 mt-0.5">{t('admin.placesEnrich.subtitle')}</p>
</div>
<ToggleSwitch
on={placesEnrichEnabled}
label={t('admin.placesEnrich.title')}
onToggle={async () => {
const next = !placesEnrichEnabled
setPlacesEnrichEnabledState(next)
setPlacesEnrichEnabled(next)
try { await adminApi.updatePlacesEnrich(next) } catch { setPlacesEnrichEnabledState(!next); setPlacesEnrichEnabled(!next) }
}}
/>
</div> </div>
{/* Open-Meteo Weather Info */} {/* Open-Meteo Weather Info */}
+5 -2
View File
@@ -46,7 +46,9 @@ export function useAdmin() {
// Places details // Places details
const [placesDetailsEnabled, setPlacesDetailsEnabledState] = useState<boolean>(true) const [placesDetailsEnabled, setPlacesDetailsEnabledState] = useState<boolean>(true)
const [placesEnrichEnabled, setPlacesEnrichEnabledState] = useState<boolean>(true)
useEffect(() => { adminApi.getPlacesDetails().then(d => setPlacesDetailsEnabledState(d.enabled)).catch(() => {}) }, []) useEffect(() => { adminApi.getPlacesDetails().then(d => setPlacesDetailsEnabledState(d.enabled)).catch(() => {}) }, [])
useEffect(() => { adminApi.getPlacesEnrich().then(d => setPlacesEnrichEnabledState(d.enabled)).catch(() => {}) }, [])
// Collab features // Collab features
const [collabFeatures, setCollabFeatures] = useState<{ chat: boolean; notes: boolean; polls: boolean; whatsnext: boolean }>({ chat: true, notes: true, polls: true, whatsnext: true }) const [collabFeatures, setCollabFeatures] = useState<{ chat: boolean; notes: boolean; polls: boolean; whatsnext: boolean }>({ chat: true, notes: true, polls: true, whatsnext: true })
@@ -107,7 +109,7 @@ export function useAdmin() {
const [updateInfo, setUpdateInfo] = useState<UpdateInfo | null>(null) const [updateInfo, setUpdateInfo] = useState<UpdateInfo | null>(null)
const [showUpdateModal, setShowUpdateModal] = useState<boolean>(false) const [showUpdateModal, setShowUpdateModal] = useState<boolean>(false)
const { user: currentUser, updateApiKeys, setAppRequireMfa, setTripRemindersEnabled, setPlacesPhotosEnabled, setPlacesAutocompleteEnabled, setPlacesDetailsEnabled, logout } = useAuthStore() const { user: currentUser, updateApiKeys, setAppRequireMfa, setTripRemindersEnabled, setPlacesPhotosEnabled, setPlacesAutocompleteEnabled, setPlacesDetailsEnabled, setPlacesEnrichEnabled, logout } = useAuthStore()
const navigate = useNavigate() const navigate = useNavigate()
const toast = useToast() const toast = useToast()
@@ -361,7 +363,7 @@ export function useAdmin() {
// store-derived // store-derived
demoMode, serverTimezone, hour12, mcpEnabled, devMode, currentUser, demoMode, serverTimezone, hour12, mcpEnabled, devMode, currentUser,
updateApiKeys, setAppRequireMfa, setTripRemindersEnabled, updateApiKeys, setAppRequireMfa, setTripRemindersEnabled,
setPlacesPhotosEnabled, setPlacesAutocompleteEnabled, setPlacesDetailsEnabled, logout, setPlacesPhotosEnabled, setPlacesAutocompleteEnabled, setPlacesDetailsEnabled, setPlacesEnrichEnabled, logout,
navigate, toast, navigate, toast,
// state + setters // state + setters
activeTab, setActiveTab, users, setUsers, stats, isLoading, activeTab, setActiveTab, users, setUsers, stats, isLoading,
@@ -371,6 +373,7 @@ export function useAdmin() {
placesPhotosEnabled, setPlacesPhotosEnabledState, placesPhotosEnabled, setPlacesPhotosEnabledState,
placesAutocompleteEnabled, setPlacesAutocompleteEnabledState, placesAutocompleteEnabled, setPlacesAutocompleteEnabledState,
placesDetailsEnabled, setPlacesDetailsEnabledState, placesDetailsEnabled, setPlacesDetailsEnabledState,
placesEnrichEnabled, setPlacesEnrichEnabledState,
collabFeatures, setCollabFeatures, collabFeatures, setCollabFeatures,
oidcConfig, setOidcConfig, savingOidc, setSavingOidc, oidcConfig, setOidcConfig, savingOidc, setSavingOidc,
passwordLogin, setPasswordLogin, passwordRegistration, setPasswordRegistration, passwordLogin, setPasswordLogin, passwordRegistration, setPasswordRegistration,
+4
View File
@@ -50,6 +50,7 @@ interface AuthState {
placesPhotosEnabled: boolean placesPhotosEnabled: boolean
placesAutocompleteEnabled: boolean placesAutocompleteEnabled: boolean
placesDetailsEnabled: boolean placesDetailsEnabled: boolean
placesEnrichEnabled: boolean
login: (email: string, password: string, rememberMe?: boolean) => Promise<LoginResult> login: (email: string, password: string, rememberMe?: boolean) => Promise<LoginResult>
completeMfaLogin: (mfaToken: string, code: string, rememberMe?: boolean) => Promise<AuthResponse> completeMfaLogin: (mfaToken: string, code: string, rememberMe?: boolean) => Promise<AuthResponse>
@@ -73,6 +74,7 @@ interface AuthState {
setPlacesPhotosEnabled: (val: boolean) => void setPlacesPhotosEnabled: (val: boolean) => void
setPlacesAutocompleteEnabled: (val: boolean) => void setPlacesAutocompleteEnabled: (val: boolean) => void
setPlacesDetailsEnabled: (val: boolean) => void setPlacesDetailsEnabled: (val: boolean) => void
setPlacesEnrichEnabled: (val: boolean) => void
demoLogin: () => Promise<AuthResponse> demoLogin: () => Promise<AuthResponse>
} }
@@ -112,6 +114,7 @@ export const useAuthStore = create<AuthState>()(
placesPhotosEnabled: true, placesPhotosEnabled: true,
placesAutocompleteEnabled: true, placesAutocompleteEnabled: true,
placesDetailsEnabled: true, placesDetailsEnabled: true,
placesEnrichEnabled: true,
login: async (email: string, password: string, rememberMe?: boolean) => { login: async (email: string, password: string, rememberMe?: boolean) => {
authSequence++ authSequence++
@@ -340,6 +343,7 @@ export const useAuthStore = create<AuthState>()(
setPlacesPhotosEnabled: (val: boolean) => set({ placesPhotosEnabled: val }), setPlacesPhotosEnabled: (val: boolean) => set({ placesPhotosEnabled: val }),
setPlacesAutocompleteEnabled: (val: boolean) => set({ placesAutocompleteEnabled: val }), setPlacesAutocompleteEnabled: (val: boolean) => set({ placesAutocompleteEnabled: val }),
setPlacesDetailsEnabled: (val: boolean) => set({ placesDetailsEnabled: val }), setPlacesDetailsEnabled: (val: boolean) => set({ placesDetailsEnabled: val }),
setPlacesEnrichEnabled: (val: boolean) => set({ placesEnrichEnabled: val }),
demoLogin: async () => { demoLogin: async () => {
authSequence++ authSequence++
+3
View File
@@ -44,6 +44,7 @@ export function buildAdminHook(overrides: Record<string, unknown> = {}): AdminHo
setPlacesPhotosEnabled: vi.fn(), setPlacesPhotosEnabled: vi.fn(),
setPlacesAutocompleteEnabled: vi.fn(), setPlacesAutocompleteEnabled: vi.fn(),
setPlacesDetailsEnabled: vi.fn(), setPlacesDetailsEnabled: vi.fn(),
setPlacesEnrichEnabled: vi.fn(),
logout: vi.fn(async () => undefined), logout: vi.fn(async () => undefined),
navigate: vi.fn(), navigate: vi.fn(),
toast: buildAdminToast(), toast: buildAdminToast(),
@@ -73,6 +74,8 @@ export function buildAdminHook(overrides: Record<string, unknown> = {}): AdminHo
setPlacesAutocompleteEnabledState: vi.fn(), setPlacesAutocompleteEnabledState: vi.fn(),
placesDetailsEnabled: false, placesDetailsEnabled: false,
setPlacesDetailsEnabledState: vi.fn(), setPlacesDetailsEnabledState: vi.fn(),
placesEnrichEnabled: true,
setPlacesEnrichEnabledState: vi.fn(),
collabFeatures: { chat: true, notes: true, polls: true, whatsnext: true }, collabFeatures: { chat: true, notes: true, polls: true, whatsnext: true },
setCollabFeatures: vi.fn(), setCollabFeatures: vi.fn(),
+10
View File
@@ -213,6 +213,16 @@ export class AdminController {
return result; return result;
} }
@Get('places-enrich')
getPlacesEnrich() { return this.admin.getPlacesEnrich(); }
@Put('places-enrich')
updatePlacesEnrich(@CurrentUser() user: User, @Body() body: AdminFeatureToggleDto, @Req() req: Request) {
const result = this.admin.updatePlacesEnrich(body.enabled);
this.audit.writeAudit({ userId: user.id, action: 'admin.places_enrich', ip: getClientIp(req), details: { enabled: result.enabled } });
return result;
}
@Get('collab-features') @Get('collab-features')
getCollabFeatures() { return this.addons.getCollabFeatures(); } getCollabFeatures() { return this.addons.getCollabFeatures(); }
+3
View File
@@ -264,6 +264,8 @@ export class AuthService implements OnModuleInit, OnModuleDestroy {
const placesAutocompleteEnabled = placesAutocompleteSetting !== 'false'; const placesAutocompleteEnabled = placesAutocompleteSetting !== 'false';
const placesDetailsSetting = this.db.get<{ value: string }>("SELECT value FROM app_settings WHERE key = 'places_details_enabled'")?.value; const placesDetailsSetting = this.db.get<{ value: string }>("SELECT value FROM app_settings WHERE key = 'places_details_enabled'")?.value;
const placesDetailsEnabled = placesDetailsSetting !== 'false'; const placesDetailsEnabled = placesDetailsSetting !== 'false';
const placesEnrichSetting = this.db.get<{ value: string }>("SELECT value FROM app_settings WHERE key = 'places_enrich_enabled'")?.value;
const placesEnrichEnabled = placesEnrichSetting !== 'false';
const setupComplete = userCount > 0 && !this.db.get("SELECT id FROM users WHERE role = 'admin' AND must_change_password = 1 LIMIT 1"); const setupComplete = userCount > 0 && !this.db.get("SELECT id FROM users WHERE role = 'admin' AND must_change_password = 1 LIMIT 1");
return { return {
@@ -302,6 +304,7 @@ export class AuthService implements OnModuleInit, OnModuleDestroy {
places_photos_enabled: placesPhotosEnabled, places_photos_enabled: placesPhotosEnabled,
places_autocomplete_enabled: placesAutocompleteEnabled, places_autocomplete_enabled: placesAutocompleteEnabled,
places_details_enabled: placesDetailsEnabled, places_details_enabled: placesDetailsEnabled,
places_enrich_enabled: placesEnrichEnabled,
permissions: authenticatedUser ? this.permissions.getAllPermissions() : undefined, permissions: authenticatedUser ? this.permissions.getAllPermissions() : undefined,
// Case-sensitive on purpose (legacy parity). // Case-sensitive on purpose (legacy parity).
dev_mode: readEnv().app.nodeEnv === 'development', dev_mode: readEnv().app.nodeEnv === 'development',
@@ -505,6 +505,23 @@ describe('admin.bridge', () => {
describe('admin quirk fixes (post-fold)', () => { describe('admin quirk fixes (post-fold)', () => {
it('ADMIN-SVC-071b — places enrichment is fail-OPEN, so no migration is needed for it', () => {
// Deliberately the other way round from the three above: unset means on, and
// PlaceEnrichmentService reads the same setting the same way. Reading it
// fail-closed here would show "off" in the panel while the feature ran.
expect(svc.getPlacesEnrich()).toEqual({ enabled: true });
svc.updatePlacesEnrich(false);
expect(svc.getPlacesEnrich()).toEqual({ enabled: false });
svc.updatePlacesEnrich(true);
expect(svc.getPlacesEnrich()).toEqual({ enabled: true });
// Only the literal 'false' switches it off; anything else stays on.
testDb.prepare("INSERT OR REPLACE INTO app_settings (key, value) VALUES ('places_enrich_enabled', 'garbage')").run();
expect(svc.getPlacesEnrich()).toEqual({ enabled: true });
});
it('ADMIN-SVC-072 — updateUser rejects an empty username/email instead of silently no-opping', () => { it('ADMIN-SVC-072 — updateUser rejects an empty username/email instead of silently no-opping', () => {
const { user } = createUser(testDb); const { user } = createUser(testDb);
expect(updateUser(String(user.id), { username: '' }) as any).toMatchObject({ status: 400, error: 'Username cannot be empty' }); expect(updateUser(String(user.id), { username: '' }) as any).toMatchObject({ status: 400, error: 'Username cannot be empty' });
+3
View File
@@ -142,6 +142,9 @@ const admin: TranslationStrings = {
'admin.placesDetails.title': 'تفاصيل الأماكن', 'admin.placesDetails.title': 'تفاصيل الأماكن',
'admin.placesDetails.subtitle': 'admin.placesDetails.subtitle':
'جلب معلومات تفصيلية عن الأماكن (الساعات، التقييم، الموقع) من Google Places API. عطّلها للحفاظ على حصة API.', 'جلب معلومات تفصيلية عن الأماكن (الساعات، التقييم، الموقع) من Google Places API. عطّلها للحفاظ على حصة API.',
'admin.placesEnrich.title': 'إثراء الأماكن',
'admin.placesEnrich.subtitle':
'يعرض صورًا ووصفًا أثناء إضافة مكان. تُستخدم ويكيبيديا وOpenStreetMap دائمًا، وتُضاف Google عند تفعيل صور الأماكن أو تفاصيل الأماكن.',
'admin.bagTracking.title': 'تتبع الأمتعة', 'admin.bagTracking.title': 'تتبع الأمتعة',
'admin.bagTracking.subtitle': 'تفعيل الوزن وتعيين الأمتعة للعناصر', 'admin.bagTracking.subtitle': 'تفعيل الوزن وتعيين الأمتعة للعناصر',
'admin.collab.chat.title': 'الدردشة', 'admin.collab.chat.title': 'الدردشة',
+10
View File
@@ -86,6 +86,16 @@ const places: TranslationStrings = {
'يتم البحث عبر OpenStreetMap (بدون صور أو ساعات عمل أو تقييمات). أضف مفتاح Google API في الإعدادات للحصول على جميع التفاصيل.', 'يتم البحث عبر OpenStreetMap (بدون صور أو ساعات عمل أو تقييمات). أضف مفتاح Google API في الإعدادات للحصول على جميع التفاصيل.',
'places.osmActive': 'places.osmActive':
'البحث عبر OpenStreetMap (بدون صور أو تقييمات أو ساعات عمل). أضف مفتاح Google API في الإعدادات لبيانات موسعة.', 'البحث عبر OpenStreetMap (بدون صور أو تقييمات أو ساعات عمل). أضف مفتاح Google API في الإعدادات لبيانات موسعة.',
'places.details.title': 'تفاصيل المكان',
'places.details.empty': 'اختر نتيجة بحث لعرض الصور والوصف.',
'places.details.loading': 'جارٍ تحميل التفاصيل...',
'places.details.error': 'تعذّر تحميل التفاصيل.',
'places.details.disabled': 'الإثراء معطّل من قِبل المسؤول.',
'places.details.noPhotos': 'لم يتم العثور على صور لهذا المكان.',
'places.details.pickImage': 'اختر صورة',
'places.details.description': 'الوصف',
'places.details.adopt': 'استخدام هذا النص',
'places.details.adoptBlocked': 'أفرغ حقل الوصف أولًا',
'places.categoryCreateError': 'فشل إنشاء الفئة', 'places.categoryCreateError': 'فشل إنشاء الفئة',
'places.nameRequired': 'يرجى إدخال اسم', 'places.nameRequired': 'يرجى إدخال اسم',
'places.saveError': 'فشل الحفظ', 'places.saveError': 'فشل الحفظ',
+3
View File
@@ -121,6 +121,9 @@ const admin: TranslationStrings = {
'admin.placesDetails.title': 'Detalhes do Local', 'admin.placesDetails.title': 'Detalhes do Local',
'admin.placesDetails.subtitle': 'admin.placesDetails.subtitle':
'Busca informações detalhadas do local (horários, avaliação, site) da Google Places API. Desative para economizar cota da API.', 'Busca informações detalhadas do local (horários, avaliação, site) da Google Places API. Desative para economizar cota da API.',
'admin.placesEnrich.title': 'Enriquecimento de lugares',
'admin.placesEnrich.subtitle':
'Mostra imagens e uma descrição ao adicionar um lugar. Wikipédia e OpenStreetMap são sempre usados; o Google entra quando Fotos de lugares ou Detalhes de lugares estão ativos.',
'admin.bagTracking.title': 'Rastreamento de malas', 'admin.bagTracking.title': 'Rastreamento de malas',
'admin.bagTracking.subtitle': 'Ativar peso e atribuição de mala para itens da lista', 'admin.bagTracking.subtitle': 'Ativar peso e atribuição de mala para itens da lista',
'admin.collab.chat.title': 'Chat', 'admin.collab.chat.title': 'Chat',
+10
View File
@@ -86,6 +86,16 @@ const places: TranslationStrings = {
'Busca via OpenStreetMap (sem fotos, horários ou avaliações). Adicione uma chave Google nas configurações para detalhes completos.', 'Busca via OpenStreetMap (sem fotos, horários ou avaliações). Adicione uma chave Google nas configurações para detalhes completos.',
'places.osmActive': 'places.osmActive':
'Busca via OpenStreetMap (sem fotos, avaliações ou horário de funcionamento). Adicione uma chave Google em Configurações para mais dados.', 'Busca via OpenStreetMap (sem fotos, avaliações ou horário de funcionamento). Adicione uma chave Google em Configurações para mais dados.',
'places.details.title': 'Detalhes do lugar',
'places.details.empty': 'Escolha um resultado para ver imagens e uma descrição.',
'places.details.loading': 'Carregando detalhes...',
'places.details.error': 'Não foi possível carregar os detalhes.',
'places.details.disabled': 'O enriquecimento está desativado pelo administrador.',
'places.details.noPhotos': 'Nenhuma imagem encontrada para este lugar.',
'places.details.pickImage': 'Escolher uma imagem',
'places.details.description': 'Descrição',
'places.details.adopt': 'Usar este texto',
'places.details.adoptBlocked': 'Esvazie primeiro o campo de descrição',
'places.categoryCreateError': 'Falha ao criar categoria', 'places.categoryCreateError': 'Falha ao criar categoria',
'places.nameRequired': 'Digite um nome', 'places.nameRequired': 'Digite um nome',
'places.saveError': 'Falha ao salvar', 'places.saveError': 'Falha ao salvar',
+3
View File
@@ -125,6 +125,9 @@ const admin: TranslationStrings = {
'admin.placesDetails.title': 'Detalls del Lloc', 'admin.placesDetails.title': 'Detalls del Lloc',
'admin.placesDetails.subtitle': 'admin.placesDetails.subtitle':
"Obtén informació detallada del lloc (horaris, valoració, web) de la Google Places API. Desactiva per estalviar quota d'API.", "Obtén informació detallada del lloc (horaris, valoració, web) de la Google Places API. Desactiva per estalviar quota d'API.",
'admin.placesEnrich.title': 'Enriquiment de llocs',
'admin.placesEnrich.subtitle':
"Mostra imatges i una descripció en afegir un lloc. Wikipedia i OpenStreetMap s'usen sempre; Google s'hi afegeix si Fotos de llocs o Detalls de llocs estan actius.",
'admin.bagTracking.title': "Seguiment d'equipatge", 'admin.bagTracking.title': "Seguiment d'equipatge",
'admin.bagTracking.subtitle': "Activar pes i assignació d'equipatge per a articles de la llista", 'admin.bagTracking.subtitle': "Activar pes i assignació d'equipatge per a articles de la llista",
'admin.collab.chat.title': 'Xat', 'admin.collab.chat.title': 'Xat',
+10
View File
@@ -84,6 +84,16 @@ const places: TranslationStrings = {
"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.", "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': 'places.osmActive':
'Cerca mitjançant OpenStreetMap (sense fotos, valoracions ni horaris). Afegeix una clau API de Google a Configuració per obtenir dades ampliades.', 'Cerca mitjançant OpenStreetMap (sense fotos, valoracions ni horaris). Afegeix una clau API de Google a Configuració per obtenir dades ampliades.',
'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...",
'places.details.error': "No s'han pogut carregar els detalls.",
'places.details.disabled': "L'enriquiment està desactivat per l'administrador.",
'places.details.noPhotos': "No s'han trobat imatges per a aquest lloc.",
'places.details.pickImage': 'Tria una imatge',
'places.details.description': 'Descripció',
'places.details.adopt': 'Utilitza aquest text',
'places.details.adoptBlocked': 'Buida primer el camp de descripció',
'places.categoryCreateError': "No s'ha pogut crear la categoria", 'places.categoryCreateError': "No s'ha pogut crear la categoria",
'places.nameRequired': 'Introdueix un nom', 'places.nameRequired': 'Introdueix un nom',
'places.saveError': "No s'ha pogut desar", 'places.saveError': "No s'ha pogut desar",
+3
View File
@@ -122,6 +122,9 @@ const admin: TranslationStrings = {
'admin.placesDetails.title': 'Podrobnosti o místě', 'admin.placesDetails.title': 'Podrobnosti o místě',
'admin.placesDetails.subtitle': 'admin.placesDetails.subtitle':
'Načítání podrobných informací o místě (hodiny, hodnocení, web) z Google Places API. Zakázáním ušetříte kvótu API.', 'Načítání podrobných informací o místě (hodiny, hodnocení, web) z Google Places API. Zakázáním ušetříte kvótu API.',
'admin.placesEnrich.title': 'Obohacení míst',
'admin.placesEnrich.subtitle':
'Zobrazí obrázky a popis při přidávání místa. Wikipedie a OpenStreetMap se používají vždy; Google se přidá, když jsou zapnuté Fotky míst nebo Detaily míst.',
'admin.bagTracking.title': 'Sledování zavazadel', 'admin.bagTracking.title': 'Sledování zavazadel',
'admin.bagTracking.subtitle': 'Povolit váhu a přiřazení k zavazadlům u položek balení', 'admin.bagTracking.subtitle': 'Povolit váhu a přiřazení k zavazadlům u položek balení',
'admin.collab.chat.title': 'Chat', 'admin.collab.chat.title': 'Chat',
+10
View File
@@ -85,6 +85,16 @@ const places: TranslationStrings = {
'places.osmHint': 'places.osmHint':
'Používáte hledání přes OpenStreetMap (bez fotek a hodnocení). Pro plné detaily přidejte Google API klíč v nastavení.', 'Používáte hledání přes OpenStreetMap (bez fotek a hodnocení). Pro plné detaily přidejte Google API klíč v nastavení.',
'places.osmActive': 'Hledání přes OpenStreetMap.', 'places.osmActive': 'Hledání přes OpenStreetMap.',
'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í...',
'places.details.error': 'Podrobnosti se nepodařilo načíst.',
'places.details.disabled': 'Obohacení je vypnuté správcem.',
'places.details.noPhotos': 'Pro toto místo nebyly nalezeny žádné obrázky.',
'places.details.pickImage': 'Vybrat obrázek',
'places.details.description': 'Popis',
'places.details.adopt': 'Použít tento text',
'places.details.adoptBlocked': 'Nejdřív vyprázdni pole popisu',
'places.categoryCreateError': 'Nepodařilo se vytvořit kategorii', 'places.categoryCreateError': 'Nepodařilo se vytvořit kategorii',
'places.nameRequired': 'Prosím zadejte název', 'places.nameRequired': 'Prosím zadejte název',
'places.saveError': 'Uložení se nezdařilo', 'places.saveError': 'Uložení se nezdařilo',
+3
View File
@@ -124,6 +124,9 @@ const admin: TranslationStrings = {
'admin.placesDetails.title': 'Ortsdetails', 'admin.placesDetails.title': 'Ortsdetails',
'admin.placesDetails.subtitle': 'admin.placesDetails.subtitle':
'Detaillierte Ortsinformationen (Öffnungszeiten, Bewertung, Website) von der Google Places API laden. Deaktivieren, um API-Kontingent zu sparen.', 'Detaillierte Ortsinformationen (Öffnungszeiten, Bewertung, Website) von der Google Places API laden. Deaktivieren, um API-Kontingent zu sparen.',
'admin.placesEnrich.title': 'Ortsanreicherung',
'admin.placesEnrich.subtitle':
'Bilder und eine Beschreibung beim Hinzufügen eines Ortes anzeigen. Wikipedia und OpenStreetMap werden immer genutzt; Google kommt zusätzlich dazu, wenn Ortsfotos oder Ortsdetails aktiv sind.',
'admin.bagTracking.title': 'Gepäck-Tracking', 'admin.bagTracking.title': 'Gepäck-Tracking',
'admin.bagTracking.subtitle': 'Gewicht und Gepäckstück-Zuordnung für Packlisteneinträge aktivieren', 'admin.bagTracking.subtitle': 'Gewicht und Gepäckstück-Zuordnung für Packlisteneinträge aktivieren',
'admin.collab.chat.title': 'Chat', 'admin.collab.chat.title': 'Chat',
+10
View File
@@ -86,6 +86,16 @@ const places: TranslationStrings = {
'OpenStreetMap-Suche aktiv (ohne Bilder, Öffnungszeiten, Bewertungen). Für erweiterte Daten Google API Key in den Einstellungen hinterlegen.', 'OpenStreetMap-Suche aktiv (ohne Bilder, Öffnungszeiten, Bewertungen). Für erweiterte Daten Google API Key in den Einstellungen hinterlegen.',
'places.osmActive': 'places.osmActive':
'Suche via OpenStreetMap (ohne Bilder, Bewertungen & Öffnungszeiten). Google API Key in den Einstellungen hinterlegen für erweiterte Daten.', 'Suche via OpenStreetMap (ohne Bilder, Bewertungen & Öffnungszeiten). Google API Key in den Einstellungen hinterlegen für erweiterte Daten.',
'places.details.title': 'Ortsdetails',
'places.details.empty': 'Wähle ein Suchergebnis, um Bilder und eine Beschreibung zu sehen.',
'places.details.loading': 'Details werden geladen...',
'places.details.error': 'Details konnten nicht geladen werden.',
'places.details.disabled': 'Die Anreicherung ist vom Administrator deaktiviert.',
'places.details.noPhotos': 'Keine Bilder für diesen Ort gefunden.',
'places.details.pickImage': 'Bild auswählen',
'places.details.description': 'Beschreibung',
'places.details.adopt': 'Text übernehmen',
'places.details.adoptBlocked': 'Erst das Beschreibungsfeld leeren',
'places.categoryCreateError': 'Fehler beim Erstellen der Kategorie', 'places.categoryCreateError': 'Fehler beim Erstellen der Kategorie',
'places.nameRequired': 'Bitte einen Namen eingeben', 'places.nameRequired': 'Bitte einen Namen eingeben',
'places.saveError': 'Fehler beim Speichern', 'places.saveError': 'Fehler beim Speichern',
+3
View File
@@ -163,6 +163,9 @@ const admin: TranslationStrings = {
'admin.placesDetails.title': 'Place Details', 'admin.placesDetails.title': 'Place Details',
'admin.placesDetails.subtitle': 'admin.placesDetails.subtitle':
'Fetch detailed place information (hours, rating, website) from the Google Places API. Disable to save API quota.', 'Fetch detailed place information (hours, rating, website) from the Google Places API. Disable to save API quota.',
'admin.placesEnrich.title': 'Place Enrichment',
'admin.placesEnrich.subtitle':
'Show pictures and a description while adding a place. Wikipedia and OpenStreetMap are always used; Google is added on top when Place Photos or Place Details are on.',
'admin.bagTracking.title': 'Bag Tracking', 'admin.bagTracking.title': 'Bag Tracking',
'admin.bagTracking.subtitle': 'Enable weight and bag assignment for packing items', 'admin.bagTracking.subtitle': 'Enable weight and bag assignment for packing items',
'admin.collab.chat.title': 'Chat', 'admin.collab.chat.title': 'Chat',
+10
View File
@@ -86,6 +86,16 @@ const places: TranslationStrings = {
'Using OpenStreetMap search (no photos, opening hours, or ratings). Add a Google API key in settings for full details.', 'Using OpenStreetMap search (no photos, opening hours, or ratings). Add a Google API key in settings for full details.',
'places.osmActive': 'places.osmActive':
'Search via OpenStreetMap (no photos, ratings or opening hours). Add a Google API key in Settings for enhanced data.', 'Search via OpenStreetMap (no photos, ratings or opening hours). Add a Google API key in Settings for enhanced data.',
'places.details.title': 'Place details',
'places.details.empty': 'Pick a search result to see pictures and a description.',
'places.details.loading': 'Loading details...',
'places.details.error': 'Details could not be loaded.',
'places.details.disabled': 'Enrichment is switched off by the administrator.',
'places.details.noPhotos': 'No pictures found for this place.',
'places.details.pickImage': 'Pick a picture',
'places.details.description': 'Description',
'places.details.adopt': 'Use this text',
'places.details.adoptBlocked': 'Clear the description field first',
'places.categoryCreateError': 'Failed to create category', 'places.categoryCreateError': 'Failed to create category',
'places.nameRequired': 'Please enter a name', 'places.nameRequired': 'Please enter a name',
'places.saveError': 'Failed to save', 'places.saveError': 'Failed to save',
+3
View File
@@ -123,6 +123,9 @@ const admin: TranslationStrings = {
'admin.placesDetails.title': 'Detalles del Lugar', 'admin.placesDetails.title': 'Detalles del Lugar',
'admin.placesDetails.subtitle': 'admin.placesDetails.subtitle':
'Obtiene información detallada del lugar (horarios, valoración, web) de la Google Places API. Desactiva para ahorrar cuota de API.', 'Obtiene información detallada del lugar (horarios, valoración, web) de la Google Places API. Desactiva para ahorrar cuota de API.',
'admin.placesEnrich.title': 'Enriquecimiento de lugares',
'admin.placesEnrich.subtitle':
'Muestra imágenes y una descripción al añadir un lugar. Wikipedia y OpenStreetMap se usan siempre; Google se añade cuando Fotos de lugares o Detalles de lugares están activados.',
'admin.bagTracking.title': 'Seguimiento de equipaje', 'admin.bagTracking.title': 'Seguimiento de equipaje',
'admin.bagTracking.subtitle': 'Activar peso y asignación de equipaje para artículos de la lista', 'admin.bagTracking.subtitle': 'Activar peso y asignación de equipaje para artículos de la lista',
'admin.collab.chat.title': 'Chat', 'admin.collab.chat.title': 'Chat',
+10
View File
@@ -86,6 +86,16 @@ const places: TranslationStrings = {
'Usando búsqueda con OpenStreetMap (sin fotos, horarios ni valoraciones). Añade una clave API de Google en Ajustes para obtener todos los detalles.', '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': 'places.osmActive':
'Búsqueda mediante OpenStreetMap (sin fotos, valoraciones ni horarios). Añade una clave API de Google en Ajustes para datos ampliados.', 'Búsqueda mediante OpenStreetMap (sin fotos, valoraciones ni horarios). Añade una clave API de Google en Ajustes para datos ampliados.',
'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...',
'places.details.error': 'No se pudieron cargar los detalles.',
'places.details.disabled': 'El administrador ha desactivado el enriquecimiento.',
'places.details.noPhotos': 'No se encontraron imágenes para este lugar.',
'places.details.pickImage': 'Elegir una imagen',
'places.details.description': 'Descripción',
'places.details.adopt': 'Usar este texto',
'places.details.adoptBlocked': 'Vacía primero el campo de descripción',
'places.categoryCreateError': 'No se pudo crear la categoría', 'places.categoryCreateError': 'No se pudo crear la categoría',
'places.nameRequired': 'Introduce un nombre', 'places.nameRequired': 'Introduce un nombre',
'places.saveError': 'No se pudo guardar', 'places.saveError': 'No se pudo guardar',
+3
View File
@@ -125,6 +125,9 @@ const admin: TranslationStrings = {
'admin.placesDetails.title': 'Détails du lieu', 'admin.placesDetails.title': 'Détails du lieu',
'admin.placesDetails.subtitle': 'admin.placesDetails.subtitle':
"Récupère les informations détaillées du lieu (horaires, note, site web) depuis l'API Google Places. Désactivez pour économiser le quota API.", "Récupère les informations détaillées du lieu (horaires, note, site web) depuis l'API Google Places. Désactivez pour économiser le quota API.",
'admin.placesEnrich.title': 'Enrichissement des lieux',
'admin.placesEnrich.subtitle':
"Affiche des images et une description lors de l'ajout d'un lieu. Wikipédia et OpenStreetMap sont toujours utilisés ; Google s'y ajoute si Photos de lieux ou Détails de lieux sont activés.",
'admin.bagTracking.title': 'Suivi des bagages', 'admin.bagTracking.title': 'Suivi des bagages',
'admin.bagTracking.subtitle': "Activer le poids et l'attribution de bagages pour les articles", 'admin.bagTracking.subtitle': "Activer le poids et l'attribution de bagages pour les articles",
'admin.collab.chat.title': 'Chat', 'admin.collab.chat.title': 'Chat',
+10
View File
@@ -86,6 +86,16 @@ const places: TranslationStrings = {
'Recherche via OpenStreetMap (pas de photos, horaires ni notes). Ajoutez une clé API Google dans les paramètres pour plus de détails.', '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': 'places.osmActive':
'Recherche via OpenStreetMap (pas de photos, notes ni horaires). Ajoutez une clé API Google dans les paramètres pour des données enrichies.', 'Recherche via OpenStreetMap (pas de photos, notes ni horaires). Ajoutez une clé API Google dans les paramètres pour des données enrichies.',
'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...',
'places.details.error': 'Impossible de charger les détails.',
'places.details.disabled': "L'enrichissement est désactivé par l'administrateur.",
'places.details.noPhotos': 'Aucune image trouvée pour ce lieu.',
'places.details.pickImage': 'Choisir une image',
'places.details.description': 'Description',
'places.details.adopt': 'Utiliser ce texte',
'places.details.adoptBlocked': "Videz d'abord le champ description",
'places.categoryCreateError': 'Impossible de créer la catégorie', 'places.categoryCreateError': 'Impossible de créer la catégorie',
'places.nameRequired': 'Veuillez saisir un nom', 'places.nameRequired': 'Veuillez saisir un nom',
'places.saveError': "Échec de l'enregistrement", 'places.saveError': "Échec de l'enregistrement",
+3
View File
@@ -170,6 +170,9 @@ const admin: TranslationStrings = {
'admin.placesDetails.title': 'Λεπτομέρειες Τόπου', 'admin.placesDetails.title': 'Λεπτομέρειες Τόπου',
'admin.placesDetails.subtitle': 'admin.placesDetails.subtitle':
'Λήψη λεπτομερών πληροφοριών τόπου (ώρες, βαθμολογία, ιστοσελίδα) από το Google Places API. Απενεργοποιήστε για εξοικονόμηση ποσοστώσης API.', 'Λήψη λεπτομερών πληροφοριών τόπου (ώρες, βαθμολογία, ιστοσελίδα) από το Google Places API. Απενεργοποιήστε για εξοικονόμηση ποσοστώσης API.',
'admin.placesEnrich.title': 'Εμπλουτισμός τοποθεσιών',
'admin.placesEnrich.subtitle':
'Εμφανίζει εικόνες και περιγραφή κατά την προσθήκη τοποθεσίας. Η Wikipedia και το OpenStreetMap χρησιμοποιούνται πάντα· η Google προστίθεται όταν είναι ενεργές οι Φωτογραφίες ή οι Λεπτομέρειες τοποθεσίας.',
'admin.bagTracking.title': 'Παρακολούθηση Αποσκευών', 'admin.bagTracking.title': 'Παρακολούθηση Αποσκευών',
'admin.bagTracking.subtitle': 'Ενεργοποίηση βάρους και ανάθεσης τσάντας για αντικείμενα πακεταρίσματος', 'admin.bagTracking.subtitle': 'Ενεργοποίηση βάρους και ανάθεσης τσάντας για αντικείμενα πακεταρίσματος',
'admin.collab.chat.title': 'Συνομιλία', 'admin.collab.chat.title': 'Συνομιλία',
+10
View File
@@ -86,6 +86,16 @@ const places: TranslationStrings = {
'Χρήση αναζήτησης OpenStreetMap (χωρίς φωτογραφίες, ώρες λειτουργίας ή αξιολογήσεις). Προσθέστε ένα κλειδί Google API στις ρυθμίσεις για πλήρεις λεπτομέρειες.', 'Χρήση αναζήτησης OpenStreetMap (χωρίς φωτογραφίες, ώρες λειτουργίας ή αξιολογήσεις). Προσθέστε ένα κλειδί Google API στις ρυθμίσεις για πλήρεις λεπτομέρειες.',
'places.osmActive': 'places.osmActive':
'Αναζήτηση μέσω OpenStreetMap (χωρίς φωτογραφίες, αξιολογήσεις ή ώρες λειτουργίας). Προσθέστε ένα κλειδί Google API στις Ρυθμίσεις για εμπλουτισμένα δεδομένα.', 'Αναζήτηση μέσω OpenStreetMap (χωρίς φωτογραφίες, αξιολογήσεις ή ώρες λειτουργίας). Προσθέστε ένα κλειδί Google API στις Ρυθμίσεις για εμπλουτισμένα δεδομένα.',
'places.details.title': 'Λεπτομέρειες τοποθεσίας',
'places.details.empty': 'Διάλεξε ένα αποτέλεσμα για να δεις εικόνες και περιγραφή.',
'places.details.loading': 'Φόρτωση λεπτομερειών...',
'places.details.error': 'Δεν ήταν δυνατή η φόρτωση των λεπτομερειών.',
'places.details.disabled': 'Ο εμπλουτισμός είναι απενεργοποιημένος από τον διαχειριστή.',
'places.details.noPhotos': 'Δεν βρέθηκαν εικόνες για αυτήν την τοποθεσία.',
'places.details.pickImage': 'Διάλεξε εικόνα',
'places.details.description': 'Περιγραφή',
'places.details.adopt': 'Χρήση αυτού του κειμένου',
'places.details.adoptBlocked': 'Άδειασε πρώτα το πεδίο περιγραφής',
'places.categoryCreateError': 'Αποτυχία δημιουργίας κατηγορίας', 'places.categoryCreateError': 'Αποτυχία δημιουργίας κατηγορίας',
'places.nameRequired': 'Παρακαλώ εισαγάγετε ένα όνομα', 'places.nameRequired': 'Παρακαλώ εισαγάγετε ένα όνομα',
'places.saveError': 'Αποτυχία αποθήκευσης', 'places.saveError': 'Αποτυχία αποθήκευσης',
+3
View File
@@ -124,6 +124,9 @@ const admin: TranslationStrings = {
'admin.placesDetails.title': 'Hely részletei', 'admin.placesDetails.title': 'Hely részletei',
'admin.placesDetails.subtitle': 'admin.placesDetails.subtitle':
'Részletes helyinformációk lekérése (nyitvatartás, értékelés, weboldal) a Google Places API-ból. Tiltsa le az API-kvóta megtakarításához.', 'Részletes helyinformációk lekérése (nyitvatartás, értékelés, weboldal) a Google Places API-ból. Tiltsa le az API-kvóta megtakarításához.',
'admin.placesEnrich.title': 'Helyek gazdagítása',
'admin.placesEnrich.subtitle':
'Képeket és leírást mutat hely hozzáadásakor. A Wikipédia és az OpenStreetMap mindig használatban van; a Google akkor jön hozzá, ha a Helyfotók vagy a Helyadatok be vannak kapcsolva.',
'admin.bagTracking.title': 'Poggyászkövetés', 'admin.bagTracking.title': 'Poggyászkövetés',
'admin.bagTracking.subtitle': 'Súly- és táskahozzárendelés engedélyezése csomagolási tételeknél', 'admin.bagTracking.subtitle': 'Súly- és táskahozzárendelés engedélyezése csomagolási tételeknél',
'admin.collab.chat.title': 'Chat', 'admin.collab.chat.title': 'Chat',
+10
View File
@@ -86,6 +86,16 @@ const places: TranslationStrings = {
'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.', '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': 'places.osmActive':
'Keresés OpenStreetMap-en keresztül (képek, értékelések és nyitvatartás nélkül). Bővített adatokhoz add meg a Google API kulcsot a beállításokban.', 'Keresés OpenStreetMap-en keresztül (képek, értékelések és nyitvatartás nélkül). Bővített adatokhoz add meg a Google API kulcsot a beállításokban.',
'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...',
'places.details.error': 'A részleteket nem sikerült betölteni.',
'places.details.disabled': 'A gazdagítást a rendszergazda kikapcsolta.',
'places.details.noPhotos': 'Nem található kép ehhez a helyhez.',
'places.details.pickImage': 'Válassz képet',
'places.details.description': 'Leírás',
'places.details.adopt': 'Szöveg átvétele',
'places.details.adoptBlocked': 'Előbb ürítsd ki a leírás mezőt',
'places.categoryCreateError': 'Nem sikerült létrehozni a kategóriát', 'places.categoryCreateError': 'Nem sikerült létrehozni a kategóriát',
'places.nameRequired': 'Kérjük, adj meg egy nevet', 'places.nameRequired': 'Kérjük, adj meg egy nevet',
'places.saveError': 'Nem sikerült menteni', 'places.saveError': 'Nem sikerült menteni',
+3
View File
@@ -166,6 +166,9 @@ const admin: TranslationStrings = {
'admin.placesDetails.title': 'Detail Tempat', 'admin.placesDetails.title': 'Detail Tempat',
'admin.placesDetails.subtitle': 'admin.placesDetails.subtitle':
'Mengambil informasi detail tempat (jam, penilaian, situs web) dari Google Places API. Nonaktifkan untuk menghemat kuota API.', 'Mengambil informasi detail tempat (jam, penilaian, situs web) dari Google Places API. Nonaktifkan untuk menghemat kuota API.',
'admin.placesEnrich.title': 'Pengayaan tempat',
'admin.placesEnrich.subtitle':
'Menampilkan gambar dan deskripsi saat menambahkan tempat. Wikipedia dan OpenStreetMap selalu digunakan; Google ditambahkan bila Foto Tempat atau Detail Tempat aktif.',
'admin.bagTracking.title': 'Pelacak Tas', 'admin.bagTracking.title': 'Pelacak Tas',
'admin.bagTracking.subtitle': 'Aktifkan berat dan penugasan tas untuk item packing', 'admin.bagTracking.subtitle': 'Aktifkan berat dan penugasan tas untuk item packing',
'admin.collab.chat.title': 'Chat', 'admin.collab.chat.title': 'Chat',
+10
View File
@@ -85,6 +85,16 @@ const places: TranslationStrings = {
'Menggunakan pencarian OpenStreetMap (tanpa foto, jam buka, atau penilaian). Tambahkan Google API key di pengaturan untuk detail lengkap.', 'Menggunakan pencarian OpenStreetMap (tanpa foto, jam buka, atau penilaian). Tambahkan Google API key di pengaturan untuk detail lengkap.',
'places.osmActive': 'places.osmActive':
'Pencarian via OpenStreetMap (tanpa foto, penilaian, atau jam buka). Tambahkan Google API key di Pengaturan untuk data yang lebih lengkap.', 'Pencarian via OpenStreetMap (tanpa foto, penilaian, atau jam buka). Tambahkan Google API key di Pengaturan untuk data yang lebih lengkap.',
'places.details.title': 'Detail tempat',
'places.details.empty': 'Pilih hasil pencarian untuk melihat gambar dan deskripsi.',
'places.details.loading': 'Memuat detail...',
'places.details.error': 'Detail tidak dapat dimuat.',
'places.details.disabled': 'Pengayaan dimatikan oleh administrator.',
'places.details.noPhotos': 'Tidak ada gambar untuk tempat ini.',
'places.details.pickImage': 'Pilih gambar',
'places.details.description': 'Deskripsi',
'places.details.adopt': 'Gunakan teks ini',
'places.details.adoptBlocked': 'Kosongkan dulu kolom deskripsi',
'places.categoryCreateError': 'Gagal membuat kategori', 'places.categoryCreateError': 'Gagal membuat kategori',
'places.nameRequired': 'Harap masukkan nama', 'places.nameRequired': 'Harap masukkan nama',
'places.saveError': 'Gagal menyimpan', 'places.saveError': 'Gagal menyimpan',
+3
View File
@@ -123,6 +123,9 @@ const admin: TranslationStrings = {
'admin.placesDetails.title': 'Dettagli del luogo', 'admin.placesDetails.title': 'Dettagli del luogo',
'admin.placesDetails.subtitle': 'admin.placesDetails.subtitle':
"Recupera informazioni dettagliate sul luogo (orari, valutazione, sito web) dall'API Google Places. Disabilita per risparmiare la quota API.", "Recupera informazioni dettagliate sul luogo (orari, valutazione, sito web) dall'API Google Places. Disabilita per risparmiare la quota API.",
'admin.placesEnrich.title': 'Arricchimento luoghi',
'admin.placesEnrich.subtitle':
'Mostra immagini e una descrizione mentre aggiungi un luogo. Wikipedia e OpenStreetMap sono sempre usati; Google si aggiunge se Foto luoghi o Dettagli luoghi sono attivi.',
'admin.bagTracking.title': 'Tracciamento valigia', 'admin.bagTracking.title': 'Tracciamento valigia',
'admin.bagTracking.subtitle': "Abilita il peso e l'assegnazione della valigia per gli elementi della lista valigia", 'admin.bagTracking.subtitle': "Abilita il peso e l'assegnazione della valigia per gli elementi della lista valigia",
'admin.collab.chat.title': 'Chat', 'admin.collab.chat.title': 'Chat',
+10
View File
@@ -86,6 +86,16 @@ const places: TranslationStrings = {
'Uso della ricerca OpenStreetMap (senza foto, orari di apertura o valutazioni). Aggiungi una chiave API Google nelle impostazioni per i dettagli completi.', 'Uso della ricerca OpenStreetMap (senza foto, orari di apertura o valutazioni). Aggiungi una chiave API Google nelle impostazioni per i dettagli completi.',
'places.osmActive': 'places.osmActive':
'Ricerca tramite OpenStreetMap (senza foto, valutazioni o orari di apertura). Aggiungi una chiave API Google nelle Impostazioni per dati avanzati.', 'Ricerca tramite OpenStreetMap (senza foto, valutazioni o orari di apertura). Aggiungi una chiave API Google nelle Impostazioni per dati avanzati.',
'places.details.title': 'Dettagli del luogo',
'places.details.empty': 'Scegli un risultato per vedere immagini e una descrizione.',
'places.details.loading': 'Caricamento dettagli...',
'places.details.error': 'Impossibile caricare i dettagli.',
'places.details.disabled': "L'arricchimento è disattivato dall'amministratore.",
'places.details.noPhotos': 'Nessuna immagine trovata per questo luogo.',
'places.details.pickImage': "Scegli un'immagine",
'places.details.description': 'Descrizione',
'places.details.adopt': 'Usa questo testo',
'places.details.adoptBlocked': 'Svuota prima il campo descrizione',
'places.categoryCreateError': 'Impossibile creare la categoria', 'places.categoryCreateError': 'Impossibile creare la categoria',
'places.nameRequired': 'Inserisci un nome', 'places.nameRequired': 'Inserisci un nome',
'places.saveError': 'Impossibile salvare', 'places.saveError': 'Impossibile salvare',
+3
View File
@@ -155,6 +155,9 @@ const admin: TranslationStrings = {
'admin.placesDetails.title': '場所の詳細', 'admin.placesDetails.title': '場所の詳細',
'admin.placesDetails.subtitle': 'admin.placesDetails.subtitle':
'Google Places APIから営業時間、評価、ウェブサイトなどの詳細情報を取得します。APIクォータ節約のため無効にできます。', 'Google Places APIから営業時間、評価、ウェブサイトなどの詳細情報を取得します。APIクォータ節約のため無効にできます。',
'admin.placesEnrich.title': '場所の情報補完',
'admin.placesEnrich.subtitle':
'場所を追加するときに写真と説明を表示します。WikipediaとOpenStreetMapは常に使用され、「場所の写真」または「場所の詳細」が有効な場合はGoogleも追加されます。',
'admin.bagTracking.title': 'バッグ管理', 'admin.bagTracking.title': 'バッグ管理',
'admin.bagTracking.subtitle': '持ち物の重量とバッグ割り当てを有効化', 'admin.bagTracking.subtitle': '持ち物の重量とバッグ割り当てを有効化',
'admin.collab.chat.title': 'チャット', 'admin.collab.chat.title': 'チャット',
+10
View File
@@ -85,6 +85,16 @@ const places: TranslationStrings = {
'places.osmHint': 'places.osmHint':
'OpenStreetMapで検索しています(写真・営業時間・評価なし)。設定でGoogle APIキーを追加すると詳細が表示されます。', 'OpenStreetMapで検索しています(写真・営業時間・評価なし)。設定でGoogle APIキーを追加すると詳細が表示されます。',
'places.osmActive': 'OpenStreetMapで検索中(写真・評価・営業時間なし)。設定でGoogle APIキーを追加してください。', 'places.osmActive': 'OpenStreetMapで検索中(写真・評価・営業時間なし)。設定でGoogle APIキーを追加してください。',
'places.details.title': '場所の詳細',
'places.details.empty': '検索結果を選ぶと写真と説明が表示されます。',
'places.details.loading': '詳細を読み込み中...',
'places.details.error': '詳細を読み込めませんでした。',
'places.details.disabled': '情報補完は管理者によって無効になっています。',
'places.details.noPhotos': 'この場所の写真は見つかりませんでした。',
'places.details.pickImage': '写真を選ぶ',
'places.details.description': '説明',
'places.details.adopt': 'このテキストを使う',
'places.details.adoptBlocked': '先に説明欄を空にしてください',
'places.categoryCreateError': 'カテゴリの作成に失敗しました', 'places.categoryCreateError': 'カテゴリの作成に失敗しました',
'places.nameRequired': '名前を入力してください', 'places.nameRequired': '名前を入力してください',
'places.saveError': '保存に失敗しました', 'places.saveError': '保存に失敗しました',
+3
View File
@@ -161,6 +161,9 @@ const admin: TranslationStrings = {
'admin.placesDetails.title': '장소 상세 정보', 'admin.placesDetails.title': '장소 상세 정보',
'admin.placesDetails.subtitle': 'admin.placesDetails.subtitle':
'Google Places API에서 상세 장소 정보 (영업 시간, 평점, 웹사이트)를 가져옵니다. API 할당량 절약을 위해 비활성화할 수 있습니다.', 'Google Places API에서 상세 장소 정보 (영업 시간, 평점, 웹사이트)를 가져옵니다. API 할당량 절약을 위해 비활성화할 수 있습니다.',
'admin.placesEnrich.title': '장소 정보 보강',
'admin.placesEnrich.subtitle':
'장소를 추가할 때 사진과 설명을 표시합니다. Wikipedia와 OpenStreetMap은 항상 사용되며, 장소 사진 또는 장소 세부정보가 켜져 있으면 Google도 함께 사용됩니다.',
'admin.bagTracking.title': '가방 추적', 'admin.bagTracking.title': '가방 추적',
'admin.bagTracking.subtitle': '짐 항목에 무게 및 가방 배정을 활성화합니다', 'admin.bagTracking.subtitle': '짐 항목에 무게 및 가방 배정을 활성화합니다',
'admin.collab.chat.title': '채팅', 'admin.collab.chat.title': '채팅',
+10
View File
@@ -85,6 +85,16 @@ const places: TranslationStrings = {
'OpenStreetMap 검색 사용 중 (사진, 영업 시간, 평점 없음). 전체 정보를 위해 설정에서 Google API 키를 추가하세요.', 'OpenStreetMap 검색 사용 중 (사진, 영업 시간, 평점 없음). 전체 정보를 위해 설정에서 Google API 키를 추가하세요.',
'places.osmActive': 'places.osmActive':
'OpenStreetMap으로 검색 중 (사진, 평점, 영업 시간 없음). 향상된 데이터를 위해 설정에서 Google API 키를 추가하세요.', 'OpenStreetMap으로 검색 중 (사진, 평점, 영업 시간 없음). 향상된 데이터를 위해 설정에서 Google API 키를 추가하세요.',
'places.details.title': '장소 세부정보',
'places.details.empty': '검색 결과를 선택하면 사진과 설명이 표시됩니다.',
'places.details.loading': '세부정보를 불러오는 중...',
'places.details.error': '세부정보를 불러오지 못했습니다.',
'places.details.disabled': '관리자가 정보 보강을 껐습니다.',
'places.details.noPhotos': '이 장소의 사진을 찾지 못했습니다.',
'places.details.pickImage': '사진 선택',
'places.details.description': '설명',
'places.details.adopt': '이 텍스트 사용',
'places.details.adoptBlocked': '설명란을 먼저 비워 주세요',
'places.categoryCreateError': '카테고리 생성 실패', 'places.categoryCreateError': '카테고리 생성 실패',
'places.nameRequired': '이름을 입력하세요', 'places.nameRequired': '이름을 입력하세요',
'places.saveError': '저장 실패', 'places.saveError': '저장 실패',
+3
View File
@@ -124,6 +124,9 @@ const admin: TranslationStrings = {
'admin.placesDetails.title': 'Plaatsdetails', 'admin.placesDetails.title': 'Plaatsdetails',
'admin.placesDetails.subtitle': 'admin.placesDetails.subtitle':
'Haalt gedetailleerde plaatsinformatie (openingstijden, beoordeling, website) op via de Google Places API. Schakel uit om API-quota te besparen.', 'Haalt gedetailleerde plaatsinformatie (openingstijden, beoordeling, website) op via de Google Places API. Schakel uit om API-quota te besparen.',
'admin.placesEnrich.title': 'Plaatsverrijking',
'admin.placesEnrich.subtitle':
"Toont afbeeldingen en een beschrijving bij het toevoegen van een plaats. Wikipedia en OpenStreetMap worden altijd gebruikt; Google komt erbij als Plaatsfoto's of Plaatsdetails aanstaan.",
'admin.bagTracking.title': 'Bagagetracking', 'admin.bagTracking.title': 'Bagagetracking',
'admin.bagTracking.subtitle': 'Gewicht en bagagetoewijzing inschakelen voor paklijstitems', 'admin.bagTracking.subtitle': 'Gewicht en bagagetoewijzing inschakelen voor paklijstitems',
'admin.collab.chat.title': 'Chat', 'admin.collab.chat.title': 'Chat',
+10
View File
@@ -86,6 +86,16 @@ const places: TranslationStrings = {
"Zoeken via OpenStreetMap (geen foto's, openingstijden of beoordelingen). Voeg een Google API-sleutel toe in instellingen voor volledige details.", "Zoeken via OpenStreetMap (geen foto's, openingstijden of beoordelingen). Voeg een Google API-sleutel toe in instellingen voor volledige details.",
'places.osmActive': 'places.osmActive':
"Zoeken via OpenStreetMap (geen foto's, beoordelingen of openingstijden). Voeg een Google API-sleutel toe in Instellingen voor uitgebreide gegevens.", "Zoeken via OpenStreetMap (geen foto's, beoordelingen of openingstijden). Voeg een Google API-sleutel toe in Instellingen voor uitgebreide gegevens.",
'places.details.title': 'Plaatsdetails',
'places.details.empty': 'Kies een zoekresultaat om afbeeldingen en een beschrijving te zien.',
'places.details.loading': 'Details laden...',
'places.details.error': 'Details konden niet worden geladen.',
'places.details.disabled': 'Verrijking is uitgeschakeld door de beheerder.',
'places.details.noPhotos': 'Geen afbeeldingen gevonden voor deze plaats.',
'places.details.pickImage': 'Kies een afbeelding',
'places.details.description': 'Beschrijving',
'places.details.adopt': 'Deze tekst gebruiken',
'places.details.adoptBlocked': 'Maak eerst het beschrijvingsveld leeg',
'places.categoryCreateError': 'Categorie aanmaken mislukt', 'places.categoryCreateError': 'Categorie aanmaken mislukt',
'places.nameRequired': 'Voer een naam in', 'places.nameRequired': 'Voer een naam in',
'places.saveError': 'Opslaan mislukt', 'places.saveError': 'Opslaan mislukt',
+3
View File
@@ -117,6 +117,9 @@ const admin: TranslationStrings = {
'admin.placesDetails.title': 'Szczegóły miejsca', 'admin.placesDetails.title': 'Szczegóły miejsca',
'admin.placesDetails.subtitle': 'admin.placesDetails.subtitle':
'Pobiera szczegółowe informacje o miejscu (godziny, ocena, strona) z Google Places API. Wyłącz, aby zaoszczędzić limit API.', 'Pobiera szczegółowe informacje o miejscu (godziny, ocena, strona) z Google Places API. Wyłącz, aby zaoszczędzić limit API.',
'admin.placesEnrich.title': 'Wzbogacanie miejsc',
'admin.placesEnrich.subtitle':
'Pokazuje zdjęcia i opis podczas dodawania miejsca. Wikipedia i OpenStreetMap są używane zawsze; Google dochodzi, gdy włączone są Zdjęcia miejsc lub Szczegóły miejsc.',
'admin.bagTracking.title': 'Kontrola bagażu', 'admin.bagTracking.title': 'Kontrola bagażu',
'admin.bagTracking.subtitle': 'Włącz wagę i przypisywanie do toreb dla przedmiotów do pakowania', 'admin.bagTracking.subtitle': 'Włącz wagę i przypisywanie do toreb dla przedmiotów do pakowania',
'admin.collab.chat.title': 'Czat', 'admin.collab.chat.title': 'Czat',
+10
View File
@@ -77,6 +77,16 @@ const places: TranslationStrings = {
'Korzystając z OpenStreetMap (brak zdjęć, godzin otwarcia czy ocen). Dodaj klucz API Google w ustawieniach aby uzyskać pełne dane.', 'Korzystając z OpenStreetMap (brak zdjęć, godzin otwarcia czy ocen). Dodaj klucz API Google w ustawieniach aby uzyskać pełne dane.',
'places.osmActive': 'places.osmActive':
'Szukaj przez OpenStreetMap (brak zdjęć, ocen czy godzin otwarcia). Dodaj klucz API Google w ustawieniach aby uzyskać pełne dane.', 'Szukaj przez OpenStreetMap (brak zdjęć, ocen czy godzin otwarcia). Dodaj klucz API Google w ustawieniach aby uzyskać pełne dane.',
'places.details.title': 'Szczegóły miejsca',
'places.details.empty': 'Wybierz wynik wyszukiwania, aby zobaczyć zdjęcia i opis.',
'places.details.loading': 'Wczytywanie szczegółów...',
'places.details.error': 'Nie udało się wczytać szczegółów.',
'places.details.disabled': 'Wzbogacanie jest wyłączone przez administratora.',
'places.details.noPhotos': 'Nie znaleziono zdjęć dla tego miejsca.',
'places.details.pickImage': 'Wybierz zdjęcie',
'places.details.description': 'Opis',
'places.details.adopt': 'Użyj tego tekstu',
'places.details.adoptBlocked': 'Najpierw wyczyść pole opisu',
'places.categoryCreateError': 'Nie udało się utworzyć kategorii', 'places.categoryCreateError': 'Nie udało się utworzyć kategorii',
'places.nameRequired': 'Proszę podać nazwę', 'places.nameRequired': 'Proszę podać nazwę',
'places.saveError': 'Nie udało się zapisać', 'places.saveError': 'Nie udało się zapisać',
+3
View File
@@ -124,6 +124,9 @@ const admin: TranslationStrings = {
'admin.placesDetails.title': 'Сведения о месте', 'admin.placesDetails.title': 'Сведения о месте',
'admin.placesDetails.subtitle': 'admin.placesDetails.subtitle':
'Загрузка подробной информации о месте (часы работы, рейтинг, веб-сайт) из Google Places API. Отключите для экономии квоты API.', 'Загрузка подробной информации о месте (часы работы, рейтинг, веб-сайт) из Google Places API. Отключите для экономии квоты API.',
'admin.placesEnrich.title': 'Обогащение мест',
'admin.placesEnrich.subtitle':
'Показывает изображения и описание при добавлении места. Wikipedia и OpenStreetMap используются всегда; Google добавляется, если включены «Фото мест» или «Детали мест».',
'admin.bagTracking.title': 'Отслеживание багажа', 'admin.bagTracking.title': 'Отслеживание багажа',
'admin.bagTracking.subtitle': 'Включить вес и привязку к багажу для вещей', 'admin.bagTracking.subtitle': 'Включить вес и привязку к багажу для вещей',
'admin.collab.chat.title': 'Чат', 'admin.collab.chat.title': 'Чат',
+10
View File
@@ -86,6 +86,16 @@ const places: TranslationStrings = {
'Поиск через OpenStreetMap (без фото, часов работы и рейтингов). Добавьте API-ключ Google в настройках для полной информации.', 'Поиск через OpenStreetMap (без фото, часов работы и рейтингов). Добавьте API-ключ Google в настройках для полной информации.',
'places.osmActive': 'places.osmActive':
'Поиск через OpenStreetMap (без фото, рейтингов и часов работы). Добавьте API-ключ Google в настройках для расширенных данных.', 'Поиск через OpenStreetMap (без фото, рейтингов и часов работы). Добавьте API-ключ Google в настройках для расширенных данных.',
'places.details.title': 'Сведения о месте',
'places.details.empty': 'Выберите результат поиска, чтобы увидеть изображения и описание.',
'places.details.loading': 'Загрузка сведений...',
'places.details.error': 'Не удалось загрузить сведения.',
'places.details.disabled': 'Обогащение отключено администратором.',
'places.details.noPhotos': 'Для этого места изображения не найдены.',
'places.details.pickImage': 'Выбрать изображение',
'places.details.description': 'Описание',
'places.details.adopt': 'Использовать этот текст',
'places.details.adoptBlocked': 'Сначала очистите поле описания',
'places.categoryCreateError': 'Не удалось создать категорию', 'places.categoryCreateError': 'Не удалось создать категорию',
'places.nameRequired': 'Введите название', 'places.nameRequired': 'Введите название',
'places.saveError': 'Ошибка сохранения', 'places.saveError': 'Ошибка сохранения',
+3
View File
@@ -167,6 +167,9 @@ const admin: TranslationStrings = {
'admin.placesDetails.title': 'Platsinformation', 'admin.placesDetails.title': 'Platsinformation',
'admin.placesDetails.subtitle': 'admin.placesDetails.subtitle':
'Hämta detaljerad information om platsen (öppettider, betyg, webbplats) från Google Places API. Inaktivera funktionen för att spara på API-kvoten.', 'Hämta detaljerad information om platsen (öppettider, betyg, webbplats) från Google Places API. Inaktivera funktionen för att spara på API-kvoten.',
'admin.placesEnrich.title': 'Platsberikning',
'admin.placesEnrich.subtitle':
'Visar bilder och en beskrivning när du lägger till en plats. Wikipedia och OpenStreetMap används alltid; Google tillkommer när Platsfoton eller Platsdetaljer är på.',
'admin.bagTracking.title': 'Spårning av väskor', 'admin.bagTracking.title': 'Spårning av väskor',
'admin.bagTracking.subtitle': 'Aktivera vikt- och väskfördelning för packningsartiklar', 'admin.bagTracking.subtitle': 'Aktivera vikt- och väskfördelning för packningsartiklar',
'admin.collab.chat.title': 'Chatt', 'admin.collab.chat.title': 'Chatt',
+10
View File
@@ -86,6 +86,16 @@ const places: TranslationStrings = {
'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.', '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': 'places.osmActive':
'Sök via OpenStreetMap (inga bilder, betyg eller öppettider). Lägg till en Google API-nyckel under Inställningar för utökad information.', 'Sök via OpenStreetMap (inga bilder, betyg eller öppettider). Lägg till en Google API-nyckel under Inställningar för utökad information.',
'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...',
'places.details.error': 'Detaljerna kunde inte laddas.',
'places.details.disabled': 'Berikning är avstängd av administratören.',
'places.details.noPhotos': 'Inga bilder hittades för den här platsen.',
'places.details.pickImage': 'Välj en bild',
'places.details.description': 'Beskrivning',
'places.details.adopt': 'Använd den här texten',
'places.details.adoptBlocked': 'Töm beskrivningsfältet först',
'places.categoryCreateError': 'Det gick inte att skapa kategorin', 'places.categoryCreateError': 'Det gick inte att skapa kategorin',
'places.nameRequired': 'Ange ett namn', 'places.nameRequired': 'Ange ett namn',
'places.saveError': 'Det gick inte att spara', 'places.saveError': 'Det gick inte att spara',
+3
View File
@@ -168,6 +168,9 @@ const admin: TranslationStrings = {
'admin.placesDetails.title': 'Yer Detayları', 'admin.placesDetails.title': 'Yer Detayları',
'admin.placesDetails.subtitle': 'admin.placesDetails.subtitle':
"Google Rehber API'sinden ayrıntılı yer bilgilerini (saat, puan, web sitesi) alın. API kotasını kaydetmeyi devre dışı bırakın.", "Google Rehber API'sinden ayrıntılı yer bilgilerini (saat, puan, web sitesi) alın. API kotasını kaydetmeyi devre dışı bırakın.",
'admin.placesEnrich.title': 'Yer zenginleştirme',
'admin.placesEnrich.subtitle':
'Bir yer eklerken görseller ve açıklama gösterir. Wikipedia ve OpenStreetMap her zaman kullanılır; Yer Fotoğrafları veya Yer Ayrıntıları açıkken Google da eklenir.',
'admin.bagTracking.title': 'Çanta Takibi', 'admin.bagTracking.title': 'Çanta Takibi',
'admin.bagTracking.subtitle': 'Paketleme öğeleri için ağırlık ve torba atamasını etkinleştirin', 'admin.bagTracking.subtitle': 'Paketleme öğeleri için ağırlık ve torba atamasını etkinleştirin',
'admin.collab.chat.title': 'Sohbet', 'admin.collab.chat.title': 'Sohbet',
+10
View File
@@ -88,6 +88,16 @@ const places: TranslationStrings = {
'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.', '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': 'places.osmActive':
"OpenStreetMap aracılığıyla arama yapın (fotoğraf, derecelendirme veya çalışma saatleri yok). Gelişmiş veriler için Ayarlar'a bir Google API anahtarı ekleyin.", "OpenStreetMap aracılığıyla arama yapın (fotoğraf, derecelendirme veya çalışma saatleri yok). Gelişmiş veriler için Ayarlar'a bir Google API anahtarı ekleyin.",
'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...',
'places.details.error': 'Ayrıntılar yüklenemedi.',
'places.details.disabled': 'Zenginleştirme yönetici tarafından kapatıldı.',
'places.details.noPhotos': 'Bu yer için görsel bulunamadı.',
'places.details.pickImage': 'Bir görsel seç',
'places.details.description': 'Açıklama',
'places.details.adopt': 'Bu metni kullan',
'places.details.adoptBlocked': 'Önce açıklama alanını boşalt',
'places.categoryCreateError': 'Kategori oluşturulamadı', 'places.categoryCreateError': 'Kategori oluşturulamadı',
'places.nameRequired': 'Lütfen bir ad girin', 'places.nameRequired': 'Lütfen bir ad girin',
'places.saveError': 'Kaydedilemedi', 'places.saveError': 'Kaydedilemedi',
+3
View File
@@ -124,6 +124,9 @@ const admin: TranslationStrings = {
'admin.placesDetails.title': 'Відомості про місце', 'admin.placesDetails.title': 'Відомості про місце',
'admin.placesDetails.subtitle': 'admin.placesDetails.subtitle':
'Завантаження детальної інформації про місце (години роботи, рейтинг, веб-сайт) з Google Places API. Вимкніть для економії квоти API.', 'Завантаження детальної інформації про місце (години роботи, рейтинг, веб-сайт) з Google Places API. Вимкніть для економії квоти API.',
'admin.placesEnrich.title': 'Збагачення місць',
'admin.placesEnrich.subtitle':
'Показує зображення та опис під час додавання місця. Wikipedia та OpenStreetMap використовуються завжди; Google додається, якщо ввімкнено «Фото місць» або «Деталі місць».',
'admin.bagTracking.title': 'Відстеження багажу', 'admin.bagTracking.title': 'Відстеження багажу',
'admin.bagTracking.subtitle': 'Увімкнути вагу та прив’язку до багажу для речей', 'admin.bagTracking.subtitle': 'Увімкнути вагу та прив’язку до багажу для речей',
'admin.collab.chat.title': 'Чат', 'admin.collab.chat.title': 'Чат',
+10
View File
@@ -86,6 +86,16 @@ const places: TranslationStrings = {
'Пошук через OpenStreetMap (без фото, годин роботи та рейтингів). Додайте API-ключ Google у налаштуваннях для повної інформації.', 'Пошук через OpenStreetMap (без фото, годин роботи та рейтингів). Додайте API-ключ Google у налаштуваннях для повної інформації.',
'places.osmActive': 'places.osmActive':
'Пошук через OpenStreetMap (без фото, рейтингів та годин роботи). Додайте API-ключ Google у налаштуваннях для розширених даних.', 'Пошук через OpenStreetMap (без фото, рейтингів та годин роботи). Додайте API-ключ Google у налаштуваннях для розширених даних.',
'places.details.title': 'Відомості про місце',
'places.details.empty': 'Виберіть результат пошуку, щоб побачити зображення та опис.',
'places.details.loading': 'Завантаження відомостей...',
'places.details.error': 'Не вдалося завантажити відомості.',
'places.details.disabled': 'Збагачення вимкнено адміністратором.',
'places.details.noPhotos': 'Для цього місця зображень не знайдено.',
'places.details.pickImage': 'Вибрати зображення',
'places.details.description': 'Опис',
'places.details.adopt': 'Використати цей текст',
'places.details.adoptBlocked': 'Спершу очистіть поле опису',
'places.categoryCreateError': 'Не вдалося створити категорію', 'places.categoryCreateError': 'Не вдалося створити категорію',
'places.nameRequired': 'Введіть назву', 'places.nameRequired': 'Введіть назву',
'places.saveError': 'Помилка збереження', 'places.saveError': 'Помилка збереження',
+3
View File
@@ -166,6 +166,9 @@ const admin: TranslationStrings = {
'admin.placesDetails.title': 'Chi tiết địa điểm', 'admin.placesDetails.title': 'Chi tiết địa điểm',
'admin.placesDetails.subtitle': 'admin.placesDetails.subtitle':
'Tìm nạp thông tin chi tiết về địa điểm (giờ, xếp hạng, trang web) từ Google Địa điểm API. Tắt để lưu hạn ngạch API.', 'Tìm nạp thông tin chi tiết về địa điểm (giờ, xếp hạng, trang web) từ Google Địa điểm API. Tắt để lưu hạn ngạch API.',
'admin.placesEnrich.title': 'Làm giàu địa điểm',
'admin.placesEnrich.subtitle':
'Hiển thị hình ảnh và mô tả khi thêm địa điểm. Wikipedia và OpenStreetMap luôn được dùng; Google được thêm vào khi bật Ảnh địa điểm hoặc Chi tiết địa điểm.',
'admin.bagTracking.title': 'Theo dõi túi', 'admin.bagTracking.title': 'Theo dõi túi',
'admin.bagTracking.subtitle': 'Cho phép phân bổ trọng lượng và túi để đóng gói các mặt hàng', 'admin.bagTracking.subtitle': 'Cho phép phân bổ trọng lượng và túi để đóng gói các mặt hàng',
'admin.collab.chat.title': 'Trò chuyện', 'admin.collab.chat.title': 'Trò chuyện',
+10
View File
@@ -86,6 +86,16 @@ const places: TranslationStrings = {
'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.', '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': 'places.osmActive':
'Tìm kiếm qua OpenStreetMap (không có ảnh, xếp hạng hoặc giờ mở cửa). Thêm khóa Google API trong Cài đặt để có dữ liệu nâng cao.', 'Tìm kiếm qua OpenStreetMap (không có ảnh, xếp hạng hoặc giờ mở cửa). Thêm khóa Google API trong Cài đặt để có dữ liệu nâng cao.',
'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...',
'places.details.error': 'Không tải được chi tiết.',
'places.details.disabled': 'Quản trị viên đã tắt tính năng làm giàu.',
'places.details.noPhotos': 'Không tìm thấy hình ảnh cho địa điểm này.',
'places.details.pickImage': 'Chọn một hình ảnh',
'places.details.description': 'Mô tả',
'places.details.adopt': 'Dùng văn bản này',
'places.details.adoptBlocked': 'Hãy xóa trống ô mô tả trước',
'places.categoryCreateError': 'Không tạo được danh mục', 'places.categoryCreateError': 'Không tạo được danh mục',
'places.nameRequired': 'Vui lòng nhập tên', 'places.nameRequired': 'Vui lòng nhập tên',
'places.saveError': 'Không lưu được', 'places.saveError': 'Không lưu được',
+3
View File
@@ -158,6 +158,9 @@ const admin: TranslationStrings = {
'admin.placesDetails.title': '地點詳情', 'admin.placesDetails.title': '地點詳情',
'admin.placesDetails.subtitle': 'admin.placesDetails.subtitle':
'從 Google Places API 獲取地點詳細資訊(營業時間、評分、網站)。停用可節省 API 配額。', '從 Google Places API 獲取地點詳細資訊(營業時間、評分、網站)。停用可節省 API 配額。',
'admin.placesEnrich.title': '地點資訊補充',
'admin.placesEnrich.subtitle':
'新增地點時顯示圖片與描述。一律使用維基百科與 OpenStreetMap;開啟「地點照片」或「地點詳細資料」時會額外使用 Google。',
'admin.bagTracking.title': '行李追蹤', 'admin.bagTracking.title': '行李追蹤',
'admin.bagTracking.subtitle': '為打包物品啟用重量和行李分配', 'admin.bagTracking.subtitle': '為打包物品啟用重量和行李分配',
'admin.collab.chat.title': '聊天', 'admin.collab.chat.title': '聊天',
+10
View File
@@ -83,6 +83,16 @@ const places: TranslationStrings = {
'places.loadingDetails': '正在載入地點詳情…', 'places.loadingDetails': '正在載入地點詳情…',
'places.osmHint': '使用 OpenStreetMap 搜尋(無照片、營業時間或評分)。在設定中新增 Google API 金鑰以獲取完整資訊。', 'places.osmHint': '使用 OpenStreetMap 搜尋(無照片、營業時間或評分)。在設定中新增 Google API 金鑰以獲取完整資訊。',
'places.osmActive': '透過 OpenStreetMap 搜尋(無照片、評分或營業時間)。在設定中新增 Google API 金鑰以獲取增強資料。', 'places.osmActive': '透過 OpenStreetMap 搜尋(無照片、評分或營業時間)。在設定中新增 Google API 金鑰以獲取增強資料。',
'places.details.title': '地點詳細資料',
'places.details.empty': '選擇一筆搜尋結果即可查看圖片與描述。',
'places.details.loading': '正在載入詳細資料...',
'places.details.error': '無法載入詳細資料。',
'places.details.disabled': '管理員已關閉資訊補充。',
'places.details.noPhotos': '找不到此地點的圖片。',
'places.details.pickImage': '選擇圖片',
'places.details.description': '描述',
'places.details.adopt': '使用此文字',
'places.details.adoptBlocked': '請先清空描述欄位',
'places.categoryCreateError': '建立分類失敗', 'places.categoryCreateError': '建立分類失敗',
'places.nameRequired': '請輸入名稱', 'places.nameRequired': '請輸入名稱',
'places.saveError': '儲存失敗', 'places.saveError': '儲存失敗',
+3
View File
@@ -119,6 +119,9 @@ const admin: TranslationStrings = {
'admin.placesDetails.title': '地点详情', 'admin.placesDetails.title': '地点详情',
'admin.placesDetails.subtitle': 'admin.placesDetails.subtitle':
'从 Google Places API 获取地点详细信息(营业时间、评分、网站)。禁用可节省 API 配额。', '从 Google Places API 获取地点详细信息(营业时间、评分、网站)。禁用可节省 API 配额。',
'admin.placesEnrich.title': '地点信息补充',
'admin.placesEnrich.subtitle':
'添加地点时显示图片和描述。始终使用维基百科和 OpenStreetMap;开启「地点照片」或「地点详情」时会额外使用 Google。',
'admin.bagTracking.title': '行李追踪', 'admin.bagTracking.title': '行李追踪',
'admin.bagTracking.subtitle': '为打包物品启用重量和行李分配', 'admin.bagTracking.subtitle': '为打包物品启用重量和行李分配',
'admin.collab.chat.title': '聊天', 'admin.collab.chat.title': '聊天',
+10
View File
@@ -83,6 +83,16 @@ const places: TranslationStrings = {
'places.loadingDetails': '正在加载地点详情…', 'places.loadingDetails': '正在加载地点详情…',
'places.osmHint': '使用 OpenStreetMap 搜索(无照片、营业时间或评分)。在设置中添加 Google API 密钥以获取完整信息。', 'places.osmHint': '使用 OpenStreetMap 搜索(无照片、营业时间或评分)。在设置中添加 Google API 密钥以获取完整信息。',
'places.osmActive': '通过 OpenStreetMap 搜索(无照片、评分或营业时间)。在设置中添加 Google API 密钥以获取增强数据。', 'places.osmActive': '通过 OpenStreetMap 搜索(无照片、评分或营业时间)。在设置中添加 Google API 密钥以获取增强数据。',
'places.details.title': '地点详情',
'places.details.empty': '选择一条搜索结果即可查看图片和描述。',
'places.details.loading': '正在加载详情...',
'places.details.error': '无法加载详情。',
'places.details.disabled': '管理员已关闭信息补充。',
'places.details.noPhotos': '未找到该地点的图片。',
'places.details.pickImage': '选择图片',
'places.details.description': '描述',
'places.details.adopt': '使用此文本',
'places.details.adoptBlocked': '请先清空描述字段',
'places.categoryCreateError': '创建分类失败', 'places.categoryCreateError': '创建分类失败',
'places.nameRequired': '请输入名称', 'places.nameRequired': '请输入名称',
'places.saveError': '保存失败', 'places.saveError': '保存失败',