mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-19 05:26:47 +00:00
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:
+3
-2
@@ -261,7 +261,7 @@ function RouteFallback() {
|
||||
}
|
||||
|
||||
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 { loadAddons } = useAddonStore()
|
||||
const { loadPlugins } = usePluginStore()
|
||||
@@ -278,7 +278,7 @@ export default function App() {
|
||||
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)
|
||||
if (config?.dev_mode) setDevMode(true)
|
||||
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_autocomplete_enabled !== undefined) setPlacesAutocompleteEnabled(config.places_autocomplete_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?.version) {
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
channelTestResultSchema,
|
||||
mapsSearchResultSchema, mapsAutocompleteResultSchema, mapsPlaceDetailsResultSchema,
|
||||
mapsPlacePhotoResultSchema, mapsReverseResultSchema, mapsResolveUrlResultSchema,
|
||||
mapsPlaceEnrichmentResultSchema,
|
||||
type NotificationRespondRequest,
|
||||
type SettingUpsertRequest, type SettingsBulkRequest,
|
||||
type JourneyCreateRequest, type JourneyAddTripRequest,
|
||||
@@ -599,6 +600,8 @@ export const adminApi = {
|
||||
updatePlacesAutocomplete: (enabled: boolean) => apiClient.put('/admin/places-autocomplete', { enabled }).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),
|
||||
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),
|
||||
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),
|
||||
@@ -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) =>
|
||||
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')),
|
||||
// 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')),
|
||||
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')),
|
||||
|
||||
@@ -25,10 +25,11 @@ interface MAdminSettingsSectionProps {
|
||||
export default function MAdminSettingsSection({ admin, t }: MAdminSettingsSectionProps) {
|
||||
const {
|
||||
toast,
|
||||
setPlacesPhotosEnabled, setPlacesAutocompleteEnabled, setPlacesDetailsEnabled,
|
||||
setPlacesPhotosEnabled, setPlacesAutocompleteEnabled, setPlacesDetailsEnabled, setPlacesEnrichEnabled,
|
||||
placesPhotosEnabled, setPlacesPhotosEnabledState,
|
||||
placesAutocompleteEnabled, setPlacesAutocompleteEnabledState,
|
||||
placesDetailsEnabled, setPlacesDetailsEnabledState,
|
||||
placesEnrichEnabled, setPlacesEnrichEnabledState,
|
||||
oidcConfig, setOidcConfig, savingOidc, setSavingOidc,
|
||||
passwordLogin, setPasswordLogin, passwordRegistration, setPasswordRegistration,
|
||||
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>
|
||||
|
||||
{/* Open-Meteo weather info */}
|
||||
|
||||
@@ -353,6 +353,27 @@ describe('AdminSettingsTab', () => {
|
||||
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', () => {
|
||||
renderTab();
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import React from 'react'
|
||||
import { adminApi, authApi } from '../../api/client'
|
||||
import { getApiErrorMessage } from '../../types'
|
||||
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 { useAdmin } from './useAdmin'
|
||||
|
||||
@@ -15,10 +16,11 @@ interface AdminSettingsTabProps {
|
||||
export default function AdminSettingsTab({ admin, t }: AdminSettingsTabProps): React.ReactElement {
|
||||
const {
|
||||
toast,
|
||||
setPlacesPhotosEnabled, setPlacesAutocompleteEnabled, setPlacesDetailsEnabled,
|
||||
setPlacesPhotosEnabled, setPlacesAutocompleteEnabled, setPlacesDetailsEnabled, setPlacesEnrichEnabled,
|
||||
placesPhotosEnabled, setPlacesPhotosEnabledState,
|
||||
placesAutocompleteEnabled, setPlacesAutocompleteEnabledState,
|
||||
placesDetailsEnabled, setPlacesDetailsEnabledState,
|
||||
placesEnrichEnabled, setPlacesEnrichEnabledState,
|
||||
oidcConfig, setOidcConfig, savingOidc, setSavingOidc,
|
||||
passwordLogin, setPasswordLogin, passwordRegistration, setPasswordRegistration,
|
||||
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-xs text-slate-400 mt-0.5">{t('admin.placesPhotos.subtitle')}</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={async () => {
|
||||
<ToggleSwitch
|
||||
on={placesPhotosEnabled}
|
||||
label={t('admin.placesPhotos.title')}
|
||||
onToggle={async () => {
|
||||
const next = !placesPhotosEnabled
|
||||
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>
|
||||
|
||||
{/* 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-xs text-slate-400 mt-0.5">{t('admin.placesAutocomplete.subtitle')}</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={async () => {
|
||||
<ToggleSwitch
|
||||
on={placesAutocompleteEnabled}
|
||||
label={t('admin.placesAutocomplete.title')}
|
||||
onToggle={async () => {
|
||||
const next = !placesAutocompleteEnabled
|
||||
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>
|
||||
|
||||
{/* 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-xs text-slate-400 mt-0.5">{t('admin.placesDetails.subtitle')}</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={async () => {
|
||||
<ToggleSwitch
|
||||
on={placesDetailsEnabled}
|
||||
label={t('admin.placesDetails.title')}
|
||||
onToggle={async () => {
|
||||
const next = !placesDetailsEnabled
|
||||
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'}`}
|
||||
>
|
||||
<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>
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 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>
|
||||
|
||||
{/* Open-Meteo Weather Info */}
|
||||
|
||||
@@ -46,7 +46,9 @@ export function useAdmin() {
|
||||
|
||||
// Places details
|
||||
const [placesDetailsEnabled, setPlacesDetailsEnabledState] = useState<boolean>(true)
|
||||
const [placesEnrichEnabled, setPlacesEnrichEnabledState] = useState<boolean>(true)
|
||||
useEffect(() => { adminApi.getPlacesDetails().then(d => setPlacesDetailsEnabledState(d.enabled)).catch(() => {}) }, [])
|
||||
useEffect(() => { adminApi.getPlacesEnrich().then(d => setPlacesEnrichEnabledState(d.enabled)).catch(() => {}) }, [])
|
||||
|
||||
// Collab features
|
||||
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 [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 toast = useToast()
|
||||
|
||||
@@ -361,7 +363,7 @@ export function useAdmin() {
|
||||
// store-derived
|
||||
demoMode, serverTimezone, hour12, mcpEnabled, devMode, currentUser,
|
||||
updateApiKeys, setAppRequireMfa, setTripRemindersEnabled,
|
||||
setPlacesPhotosEnabled, setPlacesAutocompleteEnabled, setPlacesDetailsEnabled, logout,
|
||||
setPlacesPhotosEnabled, setPlacesAutocompleteEnabled, setPlacesDetailsEnabled, setPlacesEnrichEnabled, logout,
|
||||
navigate, toast,
|
||||
// state + setters
|
||||
activeTab, setActiveTab, users, setUsers, stats, isLoading,
|
||||
@@ -371,6 +373,7 @@ export function useAdmin() {
|
||||
placesPhotosEnabled, setPlacesPhotosEnabledState,
|
||||
placesAutocompleteEnabled, setPlacesAutocompleteEnabledState,
|
||||
placesDetailsEnabled, setPlacesDetailsEnabledState,
|
||||
placesEnrichEnabled, setPlacesEnrichEnabledState,
|
||||
collabFeatures, setCollabFeatures,
|
||||
oidcConfig, setOidcConfig, savingOidc, setSavingOidc,
|
||||
passwordLogin, setPasswordLogin, passwordRegistration, setPasswordRegistration,
|
||||
|
||||
@@ -50,6 +50,7 @@ interface AuthState {
|
||||
placesPhotosEnabled: boolean
|
||||
placesAutocompleteEnabled: boolean
|
||||
placesDetailsEnabled: boolean
|
||||
placesEnrichEnabled: boolean
|
||||
|
||||
login: (email: string, password: string, rememberMe?: boolean) => Promise<LoginResult>
|
||||
completeMfaLogin: (mfaToken: string, code: string, rememberMe?: boolean) => Promise<AuthResponse>
|
||||
@@ -73,6 +74,7 @@ interface AuthState {
|
||||
setPlacesPhotosEnabled: (val: boolean) => void
|
||||
setPlacesAutocompleteEnabled: (val: boolean) => void
|
||||
setPlacesDetailsEnabled: (val: boolean) => void
|
||||
setPlacesEnrichEnabled: (val: boolean) => void
|
||||
demoLogin: () => Promise<AuthResponse>
|
||||
}
|
||||
|
||||
@@ -112,6 +114,7 @@ export const useAuthStore = create<AuthState>()(
|
||||
placesPhotosEnabled: true,
|
||||
placesAutocompleteEnabled: true,
|
||||
placesDetailsEnabled: true,
|
||||
placesEnrichEnabled: true,
|
||||
|
||||
login: async (email: string, password: string, rememberMe?: boolean) => {
|
||||
authSequence++
|
||||
@@ -340,6 +343,7 @@ export const useAuthStore = create<AuthState>()(
|
||||
setPlacesPhotosEnabled: (val: boolean) => set({ placesPhotosEnabled: val }),
|
||||
setPlacesAutocompleteEnabled: (val: boolean) => set({ placesAutocompleteEnabled: val }),
|
||||
setPlacesDetailsEnabled: (val: boolean) => set({ placesDetailsEnabled: val }),
|
||||
setPlacesEnrichEnabled: (val: boolean) => set({ placesEnrichEnabled: val }),
|
||||
|
||||
demoLogin: async () => {
|
||||
authSequence++
|
||||
|
||||
@@ -44,6 +44,7 @@ export function buildAdminHook(overrides: Record<string, unknown> = {}): AdminHo
|
||||
setPlacesPhotosEnabled: vi.fn(),
|
||||
setPlacesAutocompleteEnabled: vi.fn(),
|
||||
setPlacesDetailsEnabled: vi.fn(),
|
||||
setPlacesEnrichEnabled: vi.fn(),
|
||||
logout: vi.fn(async () => undefined),
|
||||
navigate: vi.fn(),
|
||||
toast: buildAdminToast(),
|
||||
@@ -73,6 +74,8 @@ export function buildAdminHook(overrides: Record<string, unknown> = {}): AdminHo
|
||||
setPlacesAutocompleteEnabledState: vi.fn(),
|
||||
placesDetailsEnabled: false,
|
||||
setPlacesDetailsEnabledState: vi.fn(),
|
||||
placesEnrichEnabled: true,
|
||||
setPlacesEnrichEnabledState: vi.fn(),
|
||||
collabFeatures: { chat: true, notes: true, polls: true, whatsnext: true },
|
||||
setCollabFeatures: vi.fn(),
|
||||
|
||||
|
||||
@@ -213,6 +213,16 @@ export class AdminController {
|
||||
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')
|
||||
getCollabFeatures() { return this.addons.getCollabFeatures(); }
|
||||
|
||||
|
||||
@@ -264,6 +264,8 @@ export class AuthService implements OnModuleInit, OnModuleDestroy {
|
||||
const placesAutocompleteEnabled = placesAutocompleteSetting !== 'false';
|
||||
const placesDetailsSetting = this.db.get<{ value: string }>("SELECT value FROM app_settings WHERE key = 'places_details_enabled'")?.value;
|
||||
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");
|
||||
|
||||
return {
|
||||
@@ -302,6 +304,7 @@ export class AuthService implements OnModuleInit, OnModuleDestroy {
|
||||
places_photos_enabled: placesPhotosEnabled,
|
||||
places_autocomplete_enabled: placesAutocompleteEnabled,
|
||||
places_details_enabled: placesDetailsEnabled,
|
||||
places_enrich_enabled: placesEnrichEnabled,
|
||||
permissions: authenticatedUser ? this.permissions.getAllPermissions() : undefined,
|
||||
// Case-sensitive on purpose (legacy parity).
|
||||
dev_mode: readEnv().app.nodeEnv === 'development',
|
||||
|
||||
@@ -505,6 +505,23 @@ describe('admin.bridge', () => {
|
||||
|
||||
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', () => {
|
||||
const { user } = createUser(testDb);
|
||||
expect(updateUser(String(user.id), { username: '' }) as any).toMatchObject({ status: 400, error: 'Username cannot be empty' });
|
||||
|
||||
@@ -142,6 +142,9 @@ const admin: TranslationStrings = {
|
||||
'admin.placesDetails.title': 'تفاصيل الأماكن',
|
||||
'admin.placesDetails.subtitle':
|
||||
'جلب معلومات تفصيلية عن الأماكن (الساعات، التقييم، الموقع) من Google Places API. عطّلها للحفاظ على حصة API.',
|
||||
'admin.placesEnrich.title': 'إثراء الأماكن',
|
||||
'admin.placesEnrich.subtitle':
|
||||
'يعرض صورًا ووصفًا أثناء إضافة مكان. تُستخدم ويكيبيديا وOpenStreetMap دائمًا، وتُضاف Google عند تفعيل صور الأماكن أو تفاصيل الأماكن.',
|
||||
'admin.bagTracking.title': 'تتبع الأمتعة',
|
||||
'admin.bagTracking.subtitle': 'تفعيل الوزن وتعيين الأمتعة للعناصر',
|
||||
'admin.collab.chat.title': 'الدردشة',
|
||||
|
||||
@@ -86,6 +86,16 @@ const places: TranslationStrings = {
|
||||
'يتم البحث عبر 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.nameRequired': 'يرجى إدخال اسم',
|
||||
'places.saveError': 'فشل الحفظ',
|
||||
|
||||
@@ -121,6 +121,9 @@ const admin: TranslationStrings = {
|
||||
'admin.placesDetails.title': 'Detalhes do Local',
|
||||
'admin.placesDetails.subtitle':
|
||||
'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.subtitle': 'Ativar peso e atribuição de mala para itens da lista',
|
||||
'admin.collab.chat.title': 'Chat',
|
||||
|
||||
@@ -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.',
|
||||
'places.osmActive':
|
||||
'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.nameRequired': 'Digite um nome',
|
||||
'places.saveError': 'Falha ao salvar',
|
||||
|
||||
@@ -125,6 +125,9 @@ const admin: TranslationStrings = {
|
||||
'admin.placesDetails.title': 'Detalls del Lloc',
|
||||
'admin.placesDetails.subtitle':
|
||||
"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.subtitle': "Activar pes i assignació d'equipatge per a articles de la llista",
|
||||
'admin.collab.chat.title': 'Xat',
|
||||
|
||||
@@ -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.",
|
||||
'places.osmActive':
|
||||
'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.nameRequired': 'Introdueix un nom',
|
||||
'places.saveError': "No s'ha pogut desar",
|
||||
|
||||
@@ -122,6 +122,9 @@ const admin: TranslationStrings = {
|
||||
'admin.placesDetails.title': 'Podrobnosti o místě',
|
||||
'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.',
|
||||
'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.subtitle': 'Povolit váhu a přiřazení k zavazadlům u položek balení',
|
||||
'admin.collab.chat.title': 'Chat',
|
||||
|
||||
@@ -85,6 +85,16 @@ const places: TranslationStrings = {
|
||||
'places.osmHint':
|
||||
'Používáte hledání přes OpenStreetMap (bez fotek a hodnocení). Pro plné detaily přidejte Google API klíč v nastavení.',
|
||||
'places.osmActive': '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.nameRequired': 'Prosím zadejte název',
|
||||
'places.saveError': 'Uložení se nezdařilo',
|
||||
|
||||
@@ -124,6 +124,9 @@ const admin: TranslationStrings = {
|
||||
'admin.placesDetails.title': 'Ortsdetails',
|
||||
'admin.placesDetails.subtitle':
|
||||
'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.subtitle': 'Gewicht und Gepäckstück-Zuordnung für Packlisteneinträge aktivieren',
|
||||
'admin.collab.chat.title': 'Chat',
|
||||
|
||||
@@ -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.',
|
||||
'places.osmActive':
|
||||
'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.nameRequired': 'Bitte einen Namen eingeben',
|
||||
'places.saveError': 'Fehler beim Speichern',
|
||||
|
||||
@@ -163,6 +163,9 @@ const admin: TranslationStrings = {
|
||||
'admin.placesDetails.title': 'Place Details',
|
||||
'admin.placesDetails.subtitle':
|
||||
'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.subtitle': 'Enable weight and bag assignment for packing items',
|
||||
'admin.collab.chat.title': 'Chat',
|
||||
|
||||
@@ -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.',
|
||||
'places.osmActive':
|
||||
'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.nameRequired': 'Please enter a name',
|
||||
'places.saveError': 'Failed to save',
|
||||
|
||||
@@ -123,6 +123,9 @@ const admin: TranslationStrings = {
|
||||
'admin.placesDetails.title': 'Detalles del Lugar',
|
||||
'admin.placesDetails.subtitle':
|
||||
'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.subtitle': 'Activar peso y asignación de equipaje para artículos de la lista',
|
||||
'admin.collab.chat.title': 'Chat',
|
||||
|
||||
@@ -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.',
|
||||
'places.osmActive':
|
||||
'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.nameRequired': 'Introduce un nombre',
|
||||
'places.saveError': 'No se pudo guardar',
|
||||
|
||||
@@ -125,6 +125,9 @@ const admin: TranslationStrings = {
|
||||
'admin.placesDetails.title': 'Détails du lieu',
|
||||
'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.",
|
||||
'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.subtitle': "Activer le poids et l'attribution de bagages pour les articles",
|
||||
'admin.collab.chat.title': 'Chat',
|
||||
|
||||
@@ -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.',
|
||||
'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.',
|
||||
'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.nameRequired': 'Veuillez saisir un nom',
|
||||
'places.saveError': "Échec de l'enregistrement",
|
||||
|
||||
@@ -170,6 +170,9 @@ const admin: TranslationStrings = {
|
||||
'admin.placesDetails.title': 'Λεπτομέρειες Τόπου',
|
||||
'admin.placesDetails.subtitle':
|
||||
'Λήψη λεπτομερών πληροφοριών τόπου (ώρες, βαθμολογία, ιστοσελίδα) από το Google Places API. Απενεργοποιήστε για εξοικονόμηση ποσοστώσης API.',
|
||||
'admin.placesEnrich.title': 'Εμπλουτισμός τοποθεσιών',
|
||||
'admin.placesEnrich.subtitle':
|
||||
'Εμφανίζει εικόνες και περιγραφή κατά την προσθήκη τοποθεσίας. Η Wikipedia και το OpenStreetMap χρησιμοποιούνται πάντα· η Google προστίθεται όταν είναι ενεργές οι Φωτογραφίες ή οι Λεπτομέρειες τοποθεσίας.',
|
||||
'admin.bagTracking.title': 'Παρακολούθηση Αποσκευών',
|
||||
'admin.bagTracking.subtitle': 'Ενεργοποίηση βάρους και ανάθεσης τσάντας για αντικείμενα πακεταρίσματος',
|
||||
'admin.collab.chat.title': 'Συνομιλία',
|
||||
|
||||
@@ -86,6 +86,16 @@ const places: TranslationStrings = {
|
||||
'Χρήση αναζήτησης 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.nameRequired': 'Παρακαλώ εισαγάγετε ένα όνομα',
|
||||
'places.saveError': 'Αποτυχία αποθήκευσης',
|
||||
|
||||
@@ -124,6 +124,9 @@ const admin: TranslationStrings = {
|
||||
'admin.placesDetails.title': 'Hely részletei',
|
||||
'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.',
|
||||
'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.subtitle': 'Súly- és táskahozzárendelés engedélyezése csomagolási tételeknél',
|
||||
'admin.collab.chat.title': 'Chat',
|
||||
|
||||
@@ -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.',
|
||||
'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.',
|
||||
'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.nameRequired': 'Kérjük, adj meg egy nevet',
|
||||
'places.saveError': 'Nem sikerült menteni',
|
||||
|
||||
@@ -166,6 +166,9 @@ const admin: TranslationStrings = {
|
||||
'admin.placesDetails.title': 'Detail Tempat',
|
||||
'admin.placesDetails.subtitle':
|
||||
'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.subtitle': 'Aktifkan berat dan penugasan tas untuk item packing',
|
||||
'admin.collab.chat.title': 'Chat',
|
||||
|
||||
@@ -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.',
|
||||
'places.osmActive':
|
||||
'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.nameRequired': 'Harap masukkan nama',
|
||||
'places.saveError': 'Gagal menyimpan',
|
||||
|
||||
@@ -123,6 +123,9 @@ const admin: TranslationStrings = {
|
||||
'admin.placesDetails.title': 'Dettagli del luogo',
|
||||
'admin.placesDetails.subtitle':
|
||||
"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.subtitle': "Abilita il peso e l'assegnazione della valigia per gli elementi della lista valigia",
|
||||
'admin.collab.chat.title': 'Chat',
|
||||
|
||||
@@ -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.',
|
||||
'places.osmActive':
|
||||
'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.nameRequired': 'Inserisci un nome',
|
||||
'places.saveError': 'Impossibile salvare',
|
||||
|
||||
@@ -155,6 +155,9 @@ const admin: TranslationStrings = {
|
||||
'admin.placesDetails.title': '場所の詳細',
|
||||
'admin.placesDetails.subtitle':
|
||||
'Google Places APIから営業時間、評価、ウェブサイトなどの詳細情報を取得します。APIクォータ節約のため無効にできます。',
|
||||
'admin.placesEnrich.title': '場所の情報補完',
|
||||
'admin.placesEnrich.subtitle':
|
||||
'場所を追加するときに写真と説明を表示します。WikipediaとOpenStreetMapは常に使用され、「場所の写真」または「場所の詳細」が有効な場合はGoogleも追加されます。',
|
||||
'admin.bagTracking.title': 'バッグ管理',
|
||||
'admin.bagTracking.subtitle': '持ち物の重量とバッグ割り当てを有効化',
|
||||
'admin.collab.chat.title': 'チャット',
|
||||
|
||||
@@ -85,6 +85,16 @@ const places: TranslationStrings = {
|
||||
'places.osmHint':
|
||||
'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.nameRequired': '名前を入力してください',
|
||||
'places.saveError': '保存に失敗しました',
|
||||
|
||||
@@ -161,6 +161,9 @@ const admin: TranslationStrings = {
|
||||
'admin.placesDetails.title': '장소 상세 정보',
|
||||
'admin.placesDetails.subtitle':
|
||||
'Google Places API에서 상세 장소 정보 (영업 시간, 평점, 웹사이트)를 가져옵니다. API 할당량 절약을 위해 비활성화할 수 있습니다.',
|
||||
'admin.placesEnrich.title': '장소 정보 보강',
|
||||
'admin.placesEnrich.subtitle':
|
||||
'장소를 추가할 때 사진과 설명을 표시합니다. Wikipedia와 OpenStreetMap은 항상 사용되며, 장소 사진 또는 장소 세부정보가 켜져 있으면 Google도 함께 사용됩니다.',
|
||||
'admin.bagTracking.title': '가방 추적',
|
||||
'admin.bagTracking.subtitle': '짐 항목에 무게 및 가방 배정을 활성화합니다',
|
||||
'admin.collab.chat.title': '채팅',
|
||||
|
||||
@@ -85,6 +85,16 @@ const places: TranslationStrings = {
|
||||
'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.nameRequired': '이름을 입력하세요',
|
||||
'places.saveError': '저장 실패',
|
||||
|
||||
@@ -124,6 +124,9 @@ const admin: TranslationStrings = {
|
||||
'admin.placesDetails.title': 'Plaatsdetails',
|
||||
'admin.placesDetails.subtitle':
|
||||
'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.subtitle': 'Gewicht en bagagetoewijzing inschakelen voor paklijstitems',
|
||||
'admin.collab.chat.title': 'Chat',
|
||||
|
||||
@@ -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.",
|
||||
'places.osmActive':
|
||||
"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.nameRequired': 'Voer een naam in',
|
||||
'places.saveError': 'Opslaan mislukt',
|
||||
|
||||
@@ -117,6 +117,9 @@ const admin: TranslationStrings = {
|
||||
'admin.placesDetails.title': 'Szczegóły miejsca',
|
||||
'admin.placesDetails.subtitle':
|
||||
'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.subtitle': 'Włącz wagę i przypisywanie do toreb dla przedmiotów do pakowania',
|
||||
'admin.collab.chat.title': 'Czat',
|
||||
|
||||
@@ -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.',
|
||||
'places.osmActive':
|
||||
'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.nameRequired': 'Proszę podać nazwę',
|
||||
'places.saveError': 'Nie udało się zapisać',
|
||||
|
||||
@@ -124,6 +124,9 @@ const admin: TranslationStrings = {
|
||||
'admin.placesDetails.title': 'Сведения о месте',
|
||||
'admin.placesDetails.subtitle':
|
||||
'Загрузка подробной информации о месте (часы работы, рейтинг, веб-сайт) из Google Places API. Отключите для экономии квоты API.',
|
||||
'admin.placesEnrich.title': 'Обогащение мест',
|
||||
'admin.placesEnrich.subtitle':
|
||||
'Показывает изображения и описание при добавлении места. Wikipedia и OpenStreetMap используются всегда; Google добавляется, если включены «Фото мест» или «Детали мест».',
|
||||
'admin.bagTracking.title': 'Отслеживание багажа',
|
||||
'admin.bagTracking.subtitle': 'Включить вес и привязку к багажу для вещей',
|
||||
'admin.collab.chat.title': 'Чат',
|
||||
|
||||
@@ -86,6 +86,16 @@ const places: TranslationStrings = {
|
||||
'Поиск через OpenStreetMap (без фото, часов работы и рейтингов). Добавьте API-ключ Google в настройках для полной информации.',
|
||||
'places.osmActive':
|
||||
'Поиск через 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.nameRequired': 'Введите название',
|
||||
'places.saveError': 'Ошибка сохранения',
|
||||
|
||||
@@ -167,6 +167,9 @@ const admin: TranslationStrings = {
|
||||
'admin.placesDetails.title': 'Platsinformation',
|
||||
'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.',
|
||||
'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.subtitle': 'Aktivera vikt- och väskfördelning för packningsartiklar',
|
||||
'admin.collab.chat.title': 'Chatt',
|
||||
|
||||
@@ -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.',
|
||||
'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.',
|
||||
'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.nameRequired': 'Ange ett namn',
|
||||
'places.saveError': 'Det gick inte att spara',
|
||||
|
||||
@@ -168,6 +168,9 @@ const admin: TranslationStrings = {
|
||||
'admin.placesDetails.title': 'Yer Detayları',
|
||||
'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.",
|
||||
'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.subtitle': 'Paketleme öğeleri için ağırlık ve torba atamasını etkinleştirin',
|
||||
'admin.collab.chat.title': 'Sohbet',
|
||||
|
||||
@@ -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.',
|
||||
'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.",
|
||||
'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.nameRequired': 'Lütfen bir ad girin',
|
||||
'places.saveError': 'Kaydedilemedi',
|
||||
|
||||
@@ -124,6 +124,9 @@ const admin: TranslationStrings = {
|
||||
'admin.placesDetails.title': 'Відомості про місце',
|
||||
'admin.placesDetails.subtitle':
|
||||
'Завантаження детальної інформації про місце (години роботи, рейтинг, веб-сайт) з Google Places API. Вимкніть для економії квоти API.',
|
||||
'admin.placesEnrich.title': 'Збагачення місць',
|
||||
'admin.placesEnrich.subtitle':
|
||||
'Показує зображення та опис під час додавання місця. Wikipedia та OpenStreetMap використовуються завжди; Google додається, якщо ввімкнено «Фото місць» або «Деталі місць».',
|
||||
'admin.bagTracking.title': 'Відстеження багажу',
|
||||
'admin.bagTracking.subtitle': 'Увімкнути вагу та прив’язку до багажу для речей',
|
||||
'admin.collab.chat.title': 'Чат',
|
||||
|
||||
@@ -86,6 +86,16 @@ const places: TranslationStrings = {
|
||||
'Пошук через OpenStreetMap (без фото, годин роботи та рейтингів). Додайте API-ключ Google у налаштуваннях для повної інформації.',
|
||||
'places.osmActive':
|
||||
'Пошук через 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.nameRequired': 'Введіть назву',
|
||||
'places.saveError': 'Помилка збереження',
|
||||
|
||||
@@ -166,6 +166,9 @@ const admin: TranslationStrings = {
|
||||
'admin.placesDetails.title': 'Chi tiết địa điểm',
|
||||
'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.',
|
||||
'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.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',
|
||||
|
||||
@@ -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.',
|
||||
'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.',
|
||||
'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.nameRequired': 'Vui lòng nhập tên',
|
||||
'places.saveError': 'Không lưu được',
|
||||
|
||||
@@ -158,6 +158,9 @@ const admin: TranslationStrings = {
|
||||
'admin.placesDetails.title': '地點詳情',
|
||||
'admin.placesDetails.subtitle':
|
||||
'從 Google Places API 獲取地點詳細資訊(營業時間、評分、網站)。停用可節省 API 配額。',
|
||||
'admin.placesEnrich.title': '地點資訊補充',
|
||||
'admin.placesEnrich.subtitle':
|
||||
'新增地點時顯示圖片與描述。一律使用維基百科與 OpenStreetMap;開啟「地點照片」或「地點詳細資料」時會額外使用 Google。',
|
||||
'admin.bagTracking.title': '行李追蹤',
|
||||
'admin.bagTracking.subtitle': '為打包物品啟用重量和行李分配',
|
||||
'admin.collab.chat.title': '聊天',
|
||||
|
||||
@@ -83,6 +83,16 @@ const places: TranslationStrings = {
|
||||
'places.loadingDetails': '正在載入地點詳情…',
|
||||
'places.osmHint': '使用 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.nameRequired': '請輸入名稱',
|
||||
'places.saveError': '儲存失敗',
|
||||
|
||||
@@ -119,6 +119,9 @@ const admin: TranslationStrings = {
|
||||
'admin.placesDetails.title': '地点详情',
|
||||
'admin.placesDetails.subtitle':
|
||||
'从 Google Places API 获取地点详细信息(营业时间、评分、网站)。禁用可节省 API 配额。',
|
||||
'admin.placesEnrich.title': '地点信息补充',
|
||||
'admin.placesEnrich.subtitle':
|
||||
'添加地点时显示图片和描述。始终使用维基百科和 OpenStreetMap;开启「地点照片」或「地点详情」时会额外使用 Google。',
|
||||
'admin.bagTracking.title': '行李追踪',
|
||||
'admin.bagTracking.subtitle': '为打包物品启用重量和行李分配',
|
||||
'admin.collab.chat.title': '聊天',
|
||||
|
||||
@@ -83,6 +83,16 @@ const places: TranslationStrings = {
|
||||
'places.loadingDetails': '正在加载地点详情…',
|
||||
'places.osmHint': '使用 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.nameRequired': '请输入名称',
|
||||
'places.saveError': '保存失败',
|
||||
|
||||
Reference in New Issue
Block a user