diff --git a/client/src/api/client.ts b/client/src/api/client.ts index 029da7135..d73f6d05c 100644 --- a/client/src/api/client.ts +++ b/client/src/api/client.ts @@ -648,6 +648,8 @@ export const adminApi = { updateBagTracking: (enabled: boolean) => apiClient.put('/admin/bag-tracking', { enabled }).then(r => r.data), getPlacesPhotos: () => apiClient.get('/admin/places-photos').then(r => r.data), updatePlacesPhotos: (enabled: boolean) => apiClient.put('/admin/places-photos', { enabled }).then(r => r.data), + getTrekPlaces: () => apiClient.get('/admin/trek-places').then(r => r.data), + updateTrekPlaces: (enabled: boolean) => apiClient.put('/admin/trek-places', { enabled }).then(r => r.data), getPlaceShadow: () => apiClient.get('/admin/place-shadow').then(r => r.data), updatePlaceShadow: (enabled: boolean) => apiClient.put('/admin/place-shadow', { enabled }).then(r => r.data), getPlacesAutocomplete: () => apiClient.get('/admin/places-autocomplete').then(r => r.data), diff --git a/client/src/pages/admin/AdminSettingsTab.test.tsx b/client/src/pages/admin/AdminSettingsTab.test.tsx index 37ac6c575..9924e626e 100644 --- a/client/src/pages/admin/AdminSettingsTab.test.tsx +++ b/client/src/pages/admin/AdminSettingsTab.test.tsx @@ -493,4 +493,51 @@ describe('AdminSettingsTab', () => { expect(admin.setShowRotateJwtModal).toHaveBeenCalledWith(true); }); + + describe('FE-ADMSET-041: the index switch', () => { + // The one control on this card about egress rather than about spending: + // with the index on, every search and every keystroke in the place box + // leaves the instance. The flag was read by six call paths and written by + // nothing, so before this there was no way to turn it off short of an + // INSERT against app_settings by hand. + const switchFor = () => + screen.getByRole('button', { name: /trek place index/i }); + + it('turns the index off and tells the server', async () => { + let sent: unknown = null; + server.use( + http.put('/api/admin/trek-places', async ({ request }) => { + sent = await request.json(); + return HttpResponse.json({ enabled: false }); + }), + ); + const admin = renderTab({ trekPlacesEnabled: true }); + + expect(switchFor()).toHaveAttribute('aria-pressed', 'true'); + fireEvent.click(switchFor()); + + expect(admin.setTrekPlacesEnabledState).toHaveBeenCalledWith(false); + await waitFor(() => expect(sent).toEqual({ enabled: false })); + }); + + it('rolls back when the save fails, instead of showing a state the server never took', async () => { + server.use(http.put('/api/admin/trek-places', () => HttpResponse.error())); + const admin = renderTab({ trekPlacesEnabled: true }); + + fireEvent.click(switchFor()); + + await waitFor(() => expect(admin.setTrekPlacesEnabledState).toHaveBeenLastCalledWith(true)); + }); + + it('says what answers instead, but only while it is off', () => { + renderTab({ trekPlacesEnabled: false }); + expect(screen.getByText(/place search uses openstreetmap/i)).toBeInTheDocument(); + expect(switchFor()).toHaveAttribute('aria-pressed', 'false'); + }); + + it('stays quiet about the fallback while the index is on', () => { + renderTab({ trekPlacesEnabled: true }); + expect(screen.queryByText(/place search uses openstreetmap/i)).not.toBeInTheDocument(); + }); + }); }); diff --git a/client/src/pages/admin/AdminSettingsTab.tsx b/client/src/pages/admin/AdminSettingsTab.tsx index 5a27f89ca..0fba19d74 100644 --- a/client/src/pages/admin/AdminSettingsTab.tsx +++ b/client/src/pages/admin/AdminSettingsTab.tsx @@ -26,6 +26,7 @@ export default function AdminSettingsTab({ admin, t }: AdminSettingsTabProps): R placesEnrichEnabled, setPlacesEnrichEnabledState, transitProvider, setTransitProviderState, transitGoogleKeySource, setTransitGoogleKeySource, + trekPlacesEnabled, setTrekPlacesEnabledState, placeShadowEnabled, setPlaceShadowEnabledState, oidcConfig, setOidcConfig, savingOidc, setSavingOidc, passwordLogin, setPasswordLogin, passwordRegistration, setPasswordRegistration, @@ -381,7 +382,15 @@ export default function AdminSettingsTab({ admin, t }: AdminSettingsTabProps): R {/* Three comparable blocks, in the order we would have people choose them. Weather moved out entirely: it needs no key and had nothing to configure, so it had no business in a card about keys. */} - + { + const next = !trekPlacesEnabled + setTrekPlacesEnabledState(next) + try { await adminApi.updateTrekPlaces(next) } catch { setTrekPlacesEnabledState(!next) } + }} + /> {!managed && (<> diff --git a/client/src/pages/admin/TrekApiCard.tsx b/client/src/pages/admin/TrekApiCard.tsx index e11b2a462..db30b9abf 100644 --- a/client/src/pages/admin/TrekApiCard.tsx +++ b/client/src/pages/admin/TrekApiCard.tsx @@ -1,16 +1,23 @@ import React from 'react' import { Check, ChevronRight, Globe2, KeyRound, Library, ShieldOff, WifiOff, X } from 'lucide-react' import TrekMark from '../../components/shared/TrekMark' +import ToggleSwitch from '../../components/Settings/ToggleSwitch' import type { TranslationFn } from '../../types' interface TrekApiCardProps { t: TranslationFn /** - * Whether this instance can actually use it yet. False until the client is - * wired to the service; the card then says so instead of offering a switch - * that would do nothing. + * Whether the index answers on this instance. + * + * A real switch rather than a badge, and the only one on this card that is + * about egress rather than about spending: with it on, every search and every + * keystroke in the place box leaves the instance for the service. An operator + * who does not want that has to be able to say so here — the flag was read by + * six call paths and written by nothing, so the only way to turn it off was an + * INSERT against app_settings by hand. */ - available?: boolean + enabled: boolean + onToggle: () => void } /** @@ -32,7 +39,7 @@ interface TrekApiCardProps { */ const SOURCES = ['Overture Maps Foundation', 'OpenStreetMap', 'Wikivoyage', 'Wikimedia'] -export default function TrekApiCard({ t, available = false }: TrekApiCardProps): React.ReactElement { +export default function TrekApiCard({ t, enabled, onToggle }: TrekApiCardProps): React.ReactElement { // Reuses the words TREK already has for these fields wherever it has them, // so the chip row costs two new strings instead of ten. const fields = [ @@ -77,17 +84,22 @@ export default function TrekApiCard({ t, available = false }: TrekApiCardProps):
- {available && ( - - {t('admin.trekApi.badgeActive')} - - )} +

{t('admin.trekApi.tagline')}

+ {/* Said only when it is off, and said plainly: this is the state where + searches stop using the index, and the reader should know what + answers instead rather than being left to guess. */} + {!enabled && ( +

+ {t('admin.trekApi.offNote')} +

+ )} +
    {facts.map(({ Icon, text }) => (
  • diff --git a/client/src/pages/admin/useAdmin.ts b/client/src/pages/admin/useAdmin.ts index 31ddc707a..4f79b0d6b 100644 --- a/client/src/pages/admin/useAdmin.ts +++ b/client/src/pages/admin/useAdmin.ts @@ -112,6 +112,11 @@ export function useAdmin() { }, []) // Place shadow log — off unless an admin turns it on, so the initial state is // false rather than the true the four switches above start from. + // The index switch. Read fail-open like the server does, so the state shown + // before the request lands matches what an unset row actually means. + const [trekPlacesEnabled, setTrekPlacesEnabledState] = useState(true) + useEffect(() => { adminApi.getTrekPlaces().then(d => setTrekPlacesEnabledState(d.enabled)).catch(() => {}) }, []) + const [placeShadowEnabled, setPlaceShadowEnabledState] = useState(false) useEffect(() => { adminApi.getPlaceShadow().then(d => setPlaceShadowEnabledState(d.enabled)).catch(() => {}) }, []) @@ -448,6 +453,7 @@ export function useAdmin() { placesEnrichEnabled, setPlacesEnrichEnabledState, transitProvider, setTransitProviderState, transitGoogleKeySource, setTransitGoogleKeySource, + trekPlacesEnabled, setTrekPlacesEnabledState, placeShadowEnabled, setPlaceShadowEnabledState, collabFeatures, setCollabFeatures, oidcConfig, setOidcConfig, savingOidc, setSavingOidc, diff --git a/client/tests/helpers/mobileAdmin.ts b/client/tests/helpers/mobileAdmin.ts index e3eb88bbd..636393bd4 100644 --- a/client/tests/helpers/mobileAdmin.ts +++ b/client/tests/helpers/mobileAdmin.ts @@ -76,6 +76,11 @@ export function buildAdminHook(overrides: Record = {}): AdminHo setPlacesDetailsEnabledState: vi.fn(), placesEnrichEnabled: true, setPlacesEnrichEnabledState: vi.fn(), + // Fail-open on the server, so the harness starts where an unset row lands. + trekPlacesEnabled: true, + setTrekPlacesEnabledState: vi.fn(), + placeShadowEnabled: false, + setPlaceShadowEnabledState: vi.fn(), collabFeatures: { chat: true, notes: true, polls: true, whatsnext: true }, setCollabFeatures: vi.fn(), diff --git a/server/src/nest/addons/addons.service.ts b/server/src/nest/addons/addons.service.ts index 1db74a32c..0e1ed8c26 100644 --- a/server/src/nest/addons/addons.service.ts +++ b/server/src/nest/addons/addons.service.ts @@ -218,6 +218,28 @@ export class AddonsService { updatePlacesEnrich(enabled: boolean) { return this.writeFlag('places_enrich_enabled', enabled); } + /** + * The TREK place index, read fail-OPEN for the same reason enrichment is. + * + * This is the one switch on the card that is about egress rather than about + * spending: with it on, every search and every keystroke in the place box + * leaves the instance for places.liketrek.com. An operator who does not want + * that needs somewhere to say so, and until this pair existed the key was + * read by six call paths and written by nobody — the only way to turn the + * index off was an INSERT against app_settings by hand. + * + * It has to agree with MapsService.trekPlacesEnabled(), which reads the same + * key the same way. If the two ever disagree, the panel shows "off" while the + * queries keep leaving, which is the worst of both. + */ + getTrekPlaces() { + const row = this.db.prepare("SELECT value FROM app_settings WHERE key = 'trek_places_enabled'").get() as + | { value: string } + | undefined; + return { enabled: row?.value !== 'false' }; + } + updateTrekPlaces(enabled: boolean) { return this.writeFlag('trek_places_enabled', enabled); } + // ── Transit backend (#1699) ──────────────────────────────────────────────── // Not a flag: two named backends, so it stores the name rather than a // boolean. The read/write pair lives in transit/transit-provider.ts because diff --git a/server/src/nest/admin/admin.controller.ts b/server/src/nest/admin/admin.controller.ts index 0706992c3..81fca8b9a 100644 --- a/server/src/nest/admin/admin.controller.ts +++ b/server/src/nest/admin/admin.controller.ts @@ -253,6 +253,20 @@ export class AdminController { return result; } + // The one switch here that is about egress rather than about spending: with + // the index on, every search and every keystroke in the place box leaves the + // instance. Six call paths read the key; until this pair existed nothing + // wrote it, so an operator who wanted it off had no way to say so. + @Get('trek-places') + getTrekPlaces() { return this.addons.getTrekPlaces(); } + + @Put('trek-places') + updateTrekPlaces(@CurrentUser() user: User, @Body() body: AdminFeatureToggleDto, @Req() req: Request) { + const result = this.addons.updateTrekPlaces(body.enabled); + this.audit.writeAudit({ userId: user.id, action: 'admin.trek_places', ip: getClientIp(req), details: { enabled: result.enabled } }); + return result; + } + @Get('collab-features') getCollabFeatures() { return this.addons.getCollabFeatures(); } diff --git a/server/tests/unit/nest/addons.service.test.ts b/server/tests/unit/nest/addons.service.test.ts index 4b69e2dba..fec3da4c9 100644 --- a/server/tests/unit/nest/addons.service.test.ts +++ b/server/tests/unit/nest/addons.service.test.ts @@ -4,7 +4,9 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; // A single shared statement is reused, so .all() is fed result sets in call order. const { dbMock } = vi.hoisted(() => { const stmt = { get: vi.fn(), all: vi.fn(() => []), run: vi.fn() }; - return { dbMock: { prepare: vi.fn(() => stmt), _stmt: stmt } }; + // Takes the statement text, as the real one does: a reader that inlines its + // key in the SQL is identified by that string and by nothing else. + return { dbMock: { prepare: vi.fn((_sql?: string) => stmt), _stmt: stmt } }; }); vi.mock('../../../src/db/database', () => ({ db: dbMock, closeDb: () => {}, reinitialize: () => {} })); import { db as dbConn } from '../../../src/db/database'; @@ -15,6 +17,7 @@ vi.mock('../../../src/nest/memories/memories.helpers', () => ({ getPhotoProvider import { AddonsService } from '../../../src/nest/addons/addons.service'; import { PlaceShadowService } from '../../../src/nest/place-shadow/place-shadow.service'; +import { MapsService } from '../../../src/nest/maps/maps.service'; function svc() { return new AddonsService(new DatabaseService(dbConn)); @@ -492,6 +495,91 @@ describe('AddonsService transit provider', () => { * PlaceShadowService.enabled(), which reads the same key itself — if the two * diverge the admin panel shows "off" while the log keeps collecting picks. */ +/** + * The index switch reads fail-OPEN, like places_enrich_enabled and unlike the + * three Google flags: nothing backfills a row on upgrade, so an absent row has + * to mean on, or every existing install would silently lose the index. + * + * It has to keep agreeing with MapsService.trekPlacesEnabled(), which reads the + * same key itself. If the two diverge, the admin panel shows "off" while every + * search and keystroke keeps leaving the instance. + */ +describe('AddonsService trek places flag', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('ADDONS-SVC-094 an unset flag reads as ON, and only the literal "false" turns it off', () => { + dbMock._stmt.get.mockReturnValueOnce(undefined); + expect(svc().getTrekPlaces()).toEqual({ enabled: true }); + expect(dbMock.prepare).toHaveBeenLastCalledWith( + "SELECT value FROM app_settings WHERE key = 'trek_places_enabled'", + ); + + dbMock._stmt.get.mockReturnValueOnce({ value: 'false' }); + expect(svc().getTrekPlaces()).toEqual({ enabled: false }); + + // Anything else is on. A garbled row must not silently disable the default + // search path for a whole install. + for (const value of ['true', 'FALSE', '0', '']) { + dbMock._stmt.get.mockReturnValueOnce({ value }); + expect(svc().getTrekPlaces(), value).toEqual({ enabled: true }); + } + }); + + it('ADDONS-SVC-095 the setter round-trips through the getter under its own key', () => { + // Keyed store rather than an echo assertion: the write has to produce the + // exact string the read compares against, so a setter persisting '0' fails + // here instead of silently reading back ON in production. + // + // The reader inlines its key in the SQL and binds nothing, the way the + // other fail-open flag does, so the statement text is what identifies the + // row here — not the bound parameter the writer uses. + const stored = new Map(); + let lastSql = ''; + dbMock.prepare.mockImplementation((sql: string) => { + lastSql = sql; + return dbMock._stmt; + }); + dbMock._stmt.run.mockImplementation((key: string, value: string) => { + stored.set(key, value); + }); + dbMock._stmt.get.mockImplementation((key?: string) => { + const wanted = key ?? (lastSql.match(/key = '([^']+)'/)?.[1] ?? ''); + const value = stored.get(wanted); + return value === undefined ? undefined : { value }; + }); + + expect(svc().updateTrekPlaces(false)).toEqual({ enabled: false }); + expect(dbMock._stmt.run).toHaveBeenLastCalledWith('trek_places_enabled', 'false'); + expect(svc().getTrekPlaces()).toEqual({ enabled: false }); + + expect(svc().updateTrekPlaces(true)).toEqual({ enabled: true }); + expect(dbMock._stmt.run).toHaveBeenLastCalledWith('trek_places_enabled', 'true'); + expect(svc().getTrekPlaces()).toEqual({ enabled: true }); + expect([...stored.keys()]).toEqual(['trek_places_enabled']); + }); + + it('ADDONS-SVC-096 answers the same as MapsService.trekPlacesEnabled() for every stored value', () => { + // The admin panel and the six call paths that gate on this must never + // disagree: showing "off" while queries keep leaving is worse than either + // default on its own. + const maps = new MapsService(new DatabaseService(dbConn), {} as never); + const rows: Array<[{ value: string } | undefined, boolean]> = [ + [undefined, true], + [{ value: 'false' }, false], + [{ value: 'true' }, true], + [{ value: 'garbage' }, true], + ]; + + for (const [row, expected] of rows) { + dbMock._stmt.get.mockReturnValueOnce(row).mockReturnValueOnce(row); + expect(svc().getTrekPlaces()).toEqual({ enabled: expected }); + expect(maps.trekPlacesEnabled()).toBe(expected); + } + }); +}); + describe('AddonsService place shadow flag', () => { beforeEach(() => { vi.clearAllMocks(); diff --git a/server/tests/unit/nest/admin.controller.test.ts b/server/tests/unit/nest/admin.controller.test.ts index fb420fa73..20f43d294 100644 --- a/server/tests/unit/nest/admin.controller.test.ts +++ b/server/tests/unit/nest/admin.controller.test.ts @@ -48,6 +48,9 @@ const addonsStub = () => ({ // Fail-open, unlike its three neighbours — see AddonsService.getPlacesEnrich. getPlacesEnrich: vi.fn(() => ({ enabled: true })), updatePlacesEnrich: vi.fn((enabled: boolean) => ({ enabled })), + // Fail-open too, and the only switch here about egress rather than spending. + getTrekPlaces: vi.fn(() => ({ enabled: true })), + updateTrekPlaces: vi.fn((enabled: boolean) => ({ enabled })), getCollabFeatures: vi.fn(() => ({ chat: false })), updateCollabFeatures: vi.fn(() => ({ features: { chat: true }, changed: true })), getTransitProvider: vi.fn(() => ({ provider: 'transitous', googleKeySource: null })), @@ -299,6 +302,26 @@ describe('AdminController feature toggles', () => { ); }); + it('ADMIN-TOGGLE-005 trek-places forwards the body value and audits the stored one', () => { + // The audit row carries what the service stored, not what the request + // asked for: this switch decides whether every search leaves the instance, + // so a row claiming it was turned off when it was not would be worse than + // no row at all. + const updateTrekPlaces = vi.fn(() => ({ enabled: true })); + const c = adminCtl(svc(), undefined, { ...addonsStub(), updateTrekPlaces } as unknown as AddonsService); + expect(c.updateTrekPlaces(user, { enabled: false }, req)).toEqual({ enabled: true }); + expect(updateTrekPlaces).toHaveBeenCalledWith(false); + expect(writeAudit).toHaveBeenCalledWith({ userId: user.id, action: 'admin.trek_places', ip: '1.2.3.4', details: { enabled: true } }); + }); + + it('ADMIN-TOGGLE-005b trek-places reads through to AddonsService instead of a controller-side default', () => { + const getTrekPlaces = vi.fn(() => ({ enabled: false })); + const c = adminCtl(svc(), undefined, { ...addonsStub(), getTrekPlaces } as unknown as AddonsService); + expect(c.getTrekPlaces()).toEqual({ enabled: false }); + expect(getTrekPlaces).toHaveBeenCalledTimes(1); + expect(writeAudit).not.toHaveBeenCalled(); + }); + it('ADMIN-TOGGLE-004 place-shadow forwards the body value and audits the stored one', () => { // The handler audits result.enabled. A service that declines the flip must // not leave behind an audit row claiming the shadow log was switched on. diff --git a/shared/src/i18n/ar/admin.ts b/shared/src/i18n/ar/admin.ts index 14ee16346..ef69a45ef 100644 --- a/shared/src/i18n/ar/admin.ts +++ b/shared/src/i18n/ar/admin.ts @@ -113,10 +113,10 @@ const admin: TranslationStrings = { 'admin.googleCaveat.badge': 'غير مُستحسَن', 'admin.googleCaveat.body': 'TREK مفتوح المصدر ولسنا محايدين هنا. بهذا الحجم لا توجد التقييمات وصور المحال العادية إلا لدى Google، وهذا هو الاحتكار. الحقل موجود لانعدام البديل، لا لأننا نوصي به. عندئذ يذهب كل استعلام إلى Google.', - 'admin.trekApi.badgeSoon': - 'قريبًا', - 'admin.trekApi.badgeActive': - 'مُفعَّل', + 'admin.trekApi.toggleLabel': + 'فهرس أماكن TREK', + 'admin.trekApi.offNote': + 'مُعطَّل. يستخدم البحث عن الأماكن OpenStreetMap، ويستخدم Google عند ضبط مفتاح.', 'admin.trekApi.tagline': 'فهرس الأماكن الخاص بـ TREK. بحث بلا مفتاح من Google، بلا حصة، وبلا أن يُحصي أحد عمليات بحثك.', 'admin.trekApi.factPlaces': diff --git a/shared/src/i18n/br/admin.ts b/shared/src/i18n/br/admin.ts index a3b496407..7111a4506 100644 --- a/shared/src/i18n/br/admin.ts +++ b/shared/src/i18n/br/admin.ts @@ -91,10 +91,10 @@ const admin: TranslationStrings = { 'admin.googleCaveat.badge': 'Não recomendado', 'admin.googleCaveat.body': 'O TREK é código aberto e aqui não somos neutros. Nesta escala, avaliações e fotos de estabelecimentos comuns só existem no Google, e isso é um monopólio. O campo está aqui por falta de alternativa, não porque o recomendamos. Aí cada consulta vai para o Google.', - 'admin.trekApi.badgeSoon': - 'Em breve', - 'admin.trekApi.badgeActive': - 'Ativo', + 'admin.trekApi.toggleLabel': + 'Índice de lugares do TREK', + 'admin.trekApi.offNote': + 'Desativado. A busca de lugares usa o OpenStreetMap e o Google quando há uma chave configurada.', 'admin.trekApi.tagline': 'O índice de locais do próprio TREK. Pesquisar sem chave do Google, sem cota e sem que ninguém conte as suas buscas.', 'admin.trekApi.factPlaces': diff --git a/shared/src/i18n/ca/admin.ts b/shared/src/i18n/ca/admin.ts index 70b798af8..9cf899380 100644 --- a/shared/src/i18n/ca/admin.ts +++ b/shared/src/i18n/ca/admin.ts @@ -98,10 +98,10 @@ const admin: TranslationStrings = { 'admin.googleCaveat.badge': 'No recomanat', 'admin.googleCaveat.body': "TREK és programari lliure i aquí no som neutrals. En aquesta escala, les valoracions i les fotos de negocis corrents només existeixen a Google, i això és un monopoli. El camp hi és per manca d'alternativa, no perquè el recomanem. Llavors cada consulta va a Google.", - 'admin.trekApi.badgeSoon': - 'Aviat', - 'admin.trekApi.badgeActive': - 'Actiu', + 'admin.trekApi.toggleLabel': + 'Índex de llocs de TREK', + 'admin.trekApi.offNote': + 'Desactivat. La cerca de llocs utilitza OpenStreetMap i Google si hi ha una clau configurada.', 'admin.trekApi.tagline': 'L\'índex de llocs propi de TREK. Cercar sense clau de Google, sense quota i sense que ningú compti les teves cerques.', 'admin.trekApi.factPlaces': diff --git a/shared/src/i18n/cs/admin.ts b/shared/src/i18n/cs/admin.ts index 088a7007f..a47b3683c 100644 --- a/shared/src/i18n/cs/admin.ts +++ b/shared/src/i18n/cs/admin.ts @@ -91,10 +91,10 @@ const admin: TranslationStrings = { 'admin.googleCaveat.badge': 'Nedoporučeno', 'admin.googleCaveat.body': 'TREK je otevřený software a nejsme v tom neutrální. V tomto měřítku existují hodnocení a fotografie běžných podniků jen u Googlu, a to je monopol. Pole tu je z nedostatku alternativy, ne proto, že bychom je doporučovali. Každý dotaz pak jde do Googlu.', - 'admin.trekApi.badgeSoon': - 'Brzy', - 'admin.trekApi.badgeActive': - 'Aktivní', + 'admin.trekApi.toggleLabel': + 'Index míst TREK', + 'admin.trekApi.offNote': + 'Vypnuto. Vyhledávání míst používá OpenStreetMap a Google, pokud je nastaven klíč.', 'admin.trekApi.tagline': 'Vlastní rejstřík míst TREKu. Hledání bez klíče od Googlu, bez kvóty a bez toho, aby někdo počítal vaše dotazy.', 'admin.trekApi.factPlaces': diff --git a/shared/src/i18n/de/admin.ts b/shared/src/i18n/de/admin.ts index 2ecca4c7a..3f9dca51c 100644 --- a/shared/src/i18n/de/admin.ts +++ b/shared/src/i18n/de/admin.ts @@ -93,10 +93,10 @@ const admin: TranslationStrings = { 'admin.googleCaveat.badge': 'Nicht empfohlen', 'admin.googleCaveat.body': 'TREK ist quelloffen und wir sind hier nicht neutral. Bewertungen und Fotos gewöhnlicher Läden gibt es in dieser Größenordnung nur bei Google, und genau das ist ein Monopol. Das Feld steht hier, weil es keine Alternative gibt, nicht weil wir es empfehlen. Jede Abfrage geht dann an Google.', - 'admin.trekApi.badgeSoon': - 'Bald', - 'admin.trekApi.badgeActive': - 'Aktiv', + 'admin.trekApi.toggleLabel': + 'TREK-Ortsindex', + 'admin.trekApi.offNote': + 'Ausgeschaltet. Die Ortssuche nutzt OpenStreetMap und, sofern ein Schlüssel hinterlegt ist, Google.', 'admin.trekApi.tagline': 'TREKs eigener Ortsindex. Suchen ohne Google-Schlüssel, ohne Kontingent und ohne dass jemand mitzählt.', 'admin.trekApi.factPlaces': diff --git a/shared/src/i18n/en/admin.ts b/shared/src/i18n/en/admin.ts index a27af3a04..fbde8bee8 100644 --- a/shared/src/i18n/en/admin.ts +++ b/shared/src/i18n/en/admin.ts @@ -133,10 +133,10 @@ const admin: TranslationStrings = { 'admin.googleCaveat.badge': 'Not recommended', 'admin.googleCaveat.body': 'TREK is open source and we are not neutral here. Ratings and photos of ordinary businesses exist at this scale only at Google, and that is what a monopoly is. The field is here because there is no alternative, not because we recommend it. Every lookup then goes to Google.', - 'admin.trekApi.badgeSoon': - 'Coming', - 'admin.trekApi.badgeActive': - 'Active', + 'admin.trekApi.toggleLabel': + 'TREK place index', + 'admin.trekApi.offNote': + 'Switched off. Place search uses OpenStreetMap, and Google where a key is set.', 'admin.trekApi.tagline': 'TREK\'s own place index. Search without a Google key, without a quota and without anyone counting your lookups.', 'admin.trekApi.factPlaces': diff --git a/shared/src/i18n/es/admin.ts b/shared/src/i18n/es/admin.ts index 54aa85d78..3da66d734 100644 --- a/shared/src/i18n/es/admin.ts +++ b/shared/src/i18n/es/admin.ts @@ -93,10 +93,10 @@ const admin: TranslationStrings = { 'admin.googleCaveat.badge': 'No recomendado', 'admin.googleCaveat.body': 'TREK es software libre y aquí no somos neutrales. A esta escala, las valoraciones y las fotos de negocios corrientes solo existen en Google, y eso es un monopolio. El campo está aquí por falta de alternativa, no porque lo recomendemos. Cada consulta va entonces a Google.', - 'admin.trekApi.badgeSoon': - 'Pronto', - 'admin.trekApi.badgeActive': - 'Activo', + 'admin.trekApi.toggleLabel': + 'Índice de lugares TREK', + 'admin.trekApi.offNote': + 'Desactivado. La búsqueda de lugares usa OpenStreetMap y Google si hay una clave configurada.', 'admin.trekApi.tagline': 'El índice de lugares propio de TREK. Buscar sin clave de Google, sin cuota y sin que nadie cuente tus búsquedas.', 'admin.trekApi.factPlaces': diff --git a/shared/src/i18n/fr/admin.ts b/shared/src/i18n/fr/admin.ts index 39fa2e91c..8c65706d4 100644 --- a/shared/src/i18n/fr/admin.ts +++ b/shared/src/i18n/fr/admin.ts @@ -92,10 +92,10 @@ const admin: TranslationStrings = { 'admin.googleCaveat.badge': 'Déconseillé', 'admin.googleCaveat.body': "TREK est un logiciel libre et nous ne sommes pas neutres ici. À cette échelle, les avis et les photos de commerces ordinaires n'existent que chez Google, et c'est cela, un monopole. Le champ est là faute d'alternative, pas parce que nous le recommandons. Chaque requête part alors chez Google.", - 'admin.trekApi.badgeSoon': - 'Bientôt', - 'admin.trekApi.badgeActive': - 'Actif', + 'admin.trekApi.toggleLabel': + 'Index de lieux TREK', + 'admin.trekApi.offNote': + 'Désactivé. La recherche de lieux utilise OpenStreetMap, et Google si une clé est configurée.', 'admin.trekApi.tagline': 'L\'index de lieux propre à TREK. Chercher sans clé Google, sans quota et sans que personne ne compte vos recherches.', 'admin.trekApi.factPlaces': diff --git a/shared/src/i18n/gr/admin.ts b/shared/src/i18n/gr/admin.ts index ca350e344..2e7cb4639 100644 --- a/shared/src/i18n/gr/admin.ts +++ b/shared/src/i18n/gr/admin.ts @@ -138,10 +138,10 @@ const admin: TranslationStrings = { 'admin.googleCaveat.badge': 'Δεν συνιστάται', 'admin.googleCaveat.body': 'Το TREK είναι ανοιχτού κώδικα και εδώ δεν είμαστε ουδέτεροι. Σε αυτή την κλίμακα, βαθμολογίες και φωτογραφίες συνηθισμένων καταστημάτων υπάρχουν μόνο στη Google, και αυτό είναι μονοπώλιο. Το πεδίο υπάρχει ελλείψει εναλλακτικής, όχι επειδή το συνιστούμε. Τότε κάθε αναζήτηση πηγαίνει στη Google.', - 'admin.trekApi.badgeSoon': - 'Σύντομα', - 'admin.trekApi.badgeActive': - 'Ενεργό', + 'admin.trekApi.toggleLabel': + 'Ευρετήριο τοποθεσιών TREK', + 'admin.trekApi.offNote': + 'Απενεργοποιημένο. Η αναζήτηση τοποθεσιών χρησιμοποιεί το OpenStreetMap και το Google όπου έχει οριστεί κλειδί.', 'admin.trekApi.tagline': 'Το δικό του ευρετήριο τόπων του TREK. Αναζήτηση χωρίς κλειδί Google, χωρίς όριο και χωρίς να μετράει κανείς τις αναζητήσεις σας.', 'admin.trekApi.factPlaces': diff --git a/shared/src/i18n/hu/admin.ts b/shared/src/i18n/hu/admin.ts index 64a79bd37..07ebcc6ac 100644 --- a/shared/src/i18n/hu/admin.ts +++ b/shared/src/i18n/hu/admin.ts @@ -92,10 +92,10 @@ const admin: TranslationStrings = { 'admin.googleCaveat.badge': 'Nem ajánlott', 'admin.googleCaveat.body': 'A TREK nyílt forráskódú, és ebben nem vagyunk semlegesek. Ebben a méretben az értékelések és a hétköznapi üzletek fotói csak a Google-nél léteznek, és ez a monopólium. A mező alternatíva híján van itt, nem mert ajánljuk. Minden lekérdezés ilyenkor a Google-höz megy.', - 'admin.trekApi.badgeSoon': - 'Hamarosan', - 'admin.trekApi.badgeActive': - 'Aktív', + 'admin.trekApi.toggleLabel': + 'TREK helyindex', + 'admin.trekApi.offNote': + 'Kikapcsolva. A helykeresés az OpenStreetMapot használja, és a Google-t, ha van beállított kulcs.', 'admin.trekApi.tagline': 'A TREK saját helyindexe. Keresés Google-kulcs nélkül, kvóta nélkül, és anélkül, hogy bárki számolná a lekérdezéseidet.', 'admin.trekApi.factPlaces': diff --git a/shared/src/i18n/id/admin.ts b/shared/src/i18n/id/admin.ts index 64f8c777e..9fa736f9d 100644 --- a/shared/src/i18n/id/admin.ts +++ b/shared/src/i18n/id/admin.ts @@ -134,10 +134,10 @@ const admin: TranslationStrings = { 'admin.googleCaveat.badge': 'Tidak disarankan', 'admin.googleCaveat.body': 'TREK adalah perangkat lunak sumber terbuka dan di sini kami tidak netral. Pada skala ini ulasan dan foto usaha biasa hanya ada di Google, dan itulah monopoli. Kolom ini ada karena tidak ada penggantinya, bukan karena kami menyarankan. Lalu setiap pencarian pergi ke Google.', - 'admin.trekApi.badgeSoon': - 'Segera', - 'admin.trekApi.badgeActive': - 'Aktif', + 'admin.trekApi.toggleLabel': + 'Indeks tempat TREK', + 'admin.trekApi.offNote': + 'Dimatikan. Pencarian tempat memakai OpenStreetMap, dan Google bila ada kunci yang disetel.', 'admin.trekApi.tagline': 'Indeks tempat milik TREK sendiri. Mencari tanpa kunci Google, tanpa kuota, dan tanpa ada yang menghitung pencarian Anda.', 'admin.trekApi.factPlaces': diff --git a/shared/src/i18n/it/admin.ts b/shared/src/i18n/it/admin.ts index ab7096cca..60488a250 100644 --- a/shared/src/i18n/it/admin.ts +++ b/shared/src/i18n/it/admin.ts @@ -92,10 +92,10 @@ const admin: TranslationStrings = { 'admin.googleCaveat.badge': 'Sconsigliato', 'admin.googleCaveat.body': "TREK è software libero e su questo non siamo neutrali. A questa scala recensioni e foto degli esercizi comuni esistono solo da Google, ed è questo un monopolio. Il campo c'è per mancanza di alternative, non perché lo consigliamo. Ogni interrogazione va allora a Google.", - 'admin.trekApi.badgeSoon': - 'Presto', - 'admin.trekApi.badgeActive': - 'Attivo', + 'admin.trekApi.toggleLabel': + 'Indice dei luoghi TREK', + 'admin.trekApi.offNote': + 'Disattivato. La ricerca dei luoghi usa OpenStreetMap e Google se è configurata una chiave.', 'admin.trekApi.tagline': 'L\'indice di luoghi di TREK. Cercare senza chiave Google, senza quota e senza che nessuno conti le tue ricerche.', 'admin.trekApi.factPlaces': diff --git a/shared/src/i18n/ja/admin.ts b/shared/src/i18n/ja/admin.ts index 259255fb2..ed5e3b502 100644 --- a/shared/src/i18n/ja/admin.ts +++ b/shared/src/i18n/ja/admin.ts @@ -126,10 +126,10 @@ const admin: TranslationStrings = { 'admin.googleCaveat.badge': '推奨しません', 'admin.googleCaveat.body': 'TREK はオープンソースで、この点で中立ではありません。この規模でレビューとふつうの店舗の写真があるのは Google だけで、それが独占です。この欄は代わりがないから置いてあるのであって、勧めているからではありません。使えば問い合わせはすべて Google に届きます。', - 'admin.trekApi.badgeSoon': - 'まもなく', - 'admin.trekApi.badgeActive': - '有効', + 'admin.trekApi.toggleLabel': + 'TREK 地点インデックス', + 'admin.trekApi.offNote': + 'オフです。地点検索は OpenStreetMap を使い、キーが設定されていれば Google も使います。', 'admin.trekApi.tagline': 'TREK 自前の場所インデックス。Google のキーなし、上限なし、そして誰にも検索を数えられずに探せます。', 'admin.trekApi.factPlaces': diff --git a/shared/src/i18n/ko/admin.ts b/shared/src/i18n/ko/admin.ts index 80045177e..1418399ac 100644 --- a/shared/src/i18n/ko/admin.ts +++ b/shared/src/i18n/ko/admin.ts @@ -131,10 +131,10 @@ const admin: TranslationStrings = { 'admin.googleCaveat.badge': '권장하지 않음', 'admin.googleCaveat.body': 'TREK은 오픈 소스이며 이 점에서 중립이 아닙니다. 이 규모에서 평점과 일반 상점 사진은 Google에만 있고, 그것이 독점입니다. 이 입력란은 대안이 없어서 있는 것이지 권해서가 아닙니다. 쓰면 모든 조회가 Google로 갑니다.', - 'admin.trekApi.badgeSoon': - '곧 제공', - 'admin.trekApi.badgeActive': - '사용 중', + 'admin.trekApi.toggleLabel': + 'TREK 장소 색인', + 'admin.trekApi.offNote': + '꺼져 있습니다. 장소 검색은 OpenStreetMap을 사용하며, 키가 설정되어 있으면 Google도 사용합니다.', 'admin.trekApi.tagline': 'TREK 자체 장소 색인. Google 키 없이, 할당량 없이, 누구도 검색 횟수를 세지 않는 채로 찾습니다.', 'admin.trekApi.factPlaces': diff --git a/shared/src/i18n/nl/admin.ts b/shared/src/i18n/nl/admin.ts index 03a1c1f19..f2f1a1f1c 100644 --- a/shared/src/i18n/nl/admin.ts +++ b/shared/src/i18n/nl/admin.ts @@ -93,10 +93,10 @@ const admin: TranslationStrings = { 'admin.googleCaveat.badge': 'Niet aanbevolen', 'admin.googleCaveat.body': "TREK is open source en wij zijn hier niet neutraal. Op deze schaal bestaan beoordelingen en foto's van gewone zaken alleen bij Google, en dat is een monopolie. Het veld staat er bij gebrek aan alternatief, niet omdat wij het aanraden. Elke opvraging gaat dan naar Google.", - 'admin.trekApi.badgeSoon': - 'Binnenkort', - 'admin.trekApi.badgeActive': - 'Actief', + 'admin.trekApi.toggleLabel': + 'TREK-plaatsenindex', + 'admin.trekApi.offNote': + 'Uitgeschakeld. Zoeken naar plaatsen gebruikt OpenStreetMap, en Google als er een sleutel is ingesteld.', 'admin.trekApi.tagline': 'TREK\'s eigen plaatsenindex. Zoeken zonder Google-sleutel, zonder quotum en zonder dat iemand meetelt.', 'admin.trekApi.factPlaces': diff --git a/shared/src/i18n/pl/admin.ts b/shared/src/i18n/pl/admin.ts index 5a24399ce..bc4b9f93b 100644 --- a/shared/src/i18n/pl/admin.ts +++ b/shared/src/i18n/pl/admin.ts @@ -84,10 +84,10 @@ const admin: TranslationStrings = { 'admin.googleCaveat.badge': 'Niezalecane', 'admin.googleCaveat.body': 'TREK jest otwartym oprogramowaniem i nie jesteśmy tu neutralni. W tej skali oceny i zdjęcia zwykłych lokali istnieją tylko u Google, i to jest właśnie monopol. Pole jest tu z braku alternatywy, a nie dlatego, że je zalecamy. Każde zapytanie idzie wtedy do Google.', - 'admin.trekApi.badgeSoon': - 'Wkrótce', - 'admin.trekApi.badgeActive': - 'Aktywne', + 'admin.trekApi.toggleLabel': + 'Indeks miejsc TREK', + 'admin.trekApi.offNote': + 'Wyłączone. Wyszukiwanie miejsc korzysta z OpenStreetMap oraz z Google, jeśli ustawiono klucz.', 'admin.trekApi.tagline': 'Własny indeks miejsc TREK-a. Wyszukiwanie bez klucza Google, bez limitu i bez tego, żeby ktoś liczył twoje zapytania.', 'admin.trekApi.factPlaces': diff --git a/shared/src/i18n/ru/admin.ts b/shared/src/i18n/ru/admin.ts index 9883c3490..dab6ebad1 100644 --- a/shared/src/i18n/ru/admin.ts +++ b/shared/src/i18n/ru/admin.ts @@ -93,10 +93,10 @@ const admin: TranslationStrings = { 'admin.googleCaveat.badge': 'Не рекомендуется', 'admin.googleCaveat.body': 'TREK с открытым исходным кодом, и мы здесь не нейтральны. В таком масштабе отзывы и фотографии обычных заведений есть только у Google, и это и есть монополия. Поле здесь из-за отсутствия альтернативы, а не потому, что мы его советуем. Тогда каждый запрос уходит в Google.', - 'admin.trekApi.badgeSoon': - 'Скоро', - 'admin.trekApi.badgeActive': - 'Включено', + 'admin.trekApi.toggleLabel': + 'Индекс мест TREK', + 'admin.trekApi.offNote': + 'Выключено. Поиск мест использует OpenStreetMap, а также Google, если задан ключ.', 'admin.trekApi.tagline': 'Собственный указатель мест TREK. Поиск без ключа Google, без квоты и без того, чтобы кто-то считал ваши запросы.', 'admin.trekApi.factPlaces': diff --git a/shared/src/i18n/sv/admin.ts b/shared/src/i18n/sv/admin.ts index ffeadb08c..4f951a9d7 100644 --- a/shared/src/i18n/sv/admin.ts +++ b/shared/src/i18n/sv/admin.ts @@ -135,10 +135,10 @@ const admin: TranslationStrings = { 'admin.googleCaveat.badge': 'Avrådes', 'admin.googleCaveat.body': 'TREK är öppen källkod och vi är inte neutrala här. I den här skalan finns omdömen och foton på vanliga verksamheter bara hos Google, och det är ett monopol. Fältet finns i brist på alternativ, inte för att vi rekommenderar det. Varje uppslagning går då till Google.', - 'admin.trekApi.badgeSoon': - 'Snart', - 'admin.trekApi.badgeActive': - 'Aktiv', + 'admin.trekApi.toggleLabel': + 'TREK:s platsindex', + 'admin.trekApi.offNote': + 'Avstängt. Platssökningen använder OpenStreetMap och Google om en nyckel är angiven.', 'admin.trekApi.tagline': 'TREK:s eget platsregister. Sök utan Google-nyckel, utan kvot och utan att någon räknar dina sökningar.', 'admin.trekApi.factPlaces': diff --git a/shared/src/i18n/tr/admin.ts b/shared/src/i18n/tr/admin.ts index 7325f8dcd..147c5dcee 100644 --- a/shared/src/i18n/tr/admin.ts +++ b/shared/src/i18n/tr/admin.ts @@ -135,10 +135,10 @@ const admin: TranslationStrings = { 'admin.googleCaveat.badge': 'Önerilmez', 'admin.googleCaveat.body': "TREK açık kaynaklıdır ve burada tarafsız değiliz. Bu ölçekte değerlendirmeler ve sıradan işletme fotoğrafları yalnızca Google'da vardır; tekel budur. Alan, başka seçenek olmadığı için burada; önerdiğimiz için değil. O zaman her sorgu Google'a gider.", - 'admin.trekApi.badgeSoon': - 'Yakında', - 'admin.trekApi.badgeActive': - 'Etkin', + 'admin.trekApi.toggleLabel': + 'TREK yer dizini', + 'admin.trekApi.offNote': + 'Kapalı. Yer araması OpenStreetMap kullanır, anahtar tanımlıysa Google da kullanılır.', 'admin.trekApi.tagline': 'TREK\'in kendi yer dizini. Google anahtarı olmadan, kota olmadan ve kimse aramalarınızı saymadan arama.', 'admin.trekApi.factPlaces': diff --git a/shared/src/i18n/uk/admin.ts b/shared/src/i18n/uk/admin.ts index 589d5b8ed..f51cbc6ba 100644 --- a/shared/src/i18n/uk/admin.ts +++ b/shared/src/i18n/uk/admin.ts @@ -93,10 +93,10 @@ const admin: TranslationStrings = { 'admin.googleCaveat.badge': 'Не рекомендовано', 'admin.googleCaveat.body': 'TREK має відкритий код, і ми тут не нейтральні. У такому масштабі відгуки та світлини звичайних закладів є лише в Google, і це і є монополія. Поле тут через брак альтернативи, а не тому, що ми його радимо. Тоді кожен запит іде до Google.', - 'admin.trekApi.badgeSoon': - 'Незабаром', - 'admin.trekApi.badgeActive': - 'Увімкнено', + 'admin.trekApi.toggleLabel': + 'Індекс місць TREK', + 'admin.trekApi.offNote': + 'Вимкнено. Пошук місць використовує OpenStreetMap, а також Google, якщо задано ключ.', 'admin.trekApi.tagline': 'Власний покажчик місць TREK. Пошук без ключа Google, без квоти й без того, щоб хтось рахував ваші запити.', 'admin.trekApi.factPlaces': diff --git a/shared/src/i18n/vi/admin.ts b/shared/src/i18n/vi/admin.ts index 72e2eb0a6..05dc628d5 100644 --- a/shared/src/i18n/vi/admin.ts +++ b/shared/src/i18n/vi/admin.ts @@ -134,10 +134,10 @@ const admin: TranslationStrings = { 'admin.googleCaveat.badge': 'Không khuyến nghị', 'admin.googleCaveat.body': 'TREK là phần mềm nguồn mở và ở đây chúng tôi không trung lập. Ở quy mô này, đánh giá và ảnh của các cửa hàng thông thường chỉ có ở Google, và đó là độc quyền. Ô này có mặt vì không có lựa chọn khác, không phải vì chúng tôi khuyến nghị. Khi đó mọi truy vấn đều đi tới Google.', - 'admin.trekApi.badgeSoon': - 'Sắp có', - 'admin.trekApi.badgeActive': - 'Đang bật', + 'admin.trekApi.toggleLabel': + 'Chỉ mục địa điểm TREK', + 'admin.trekApi.offNote': + 'Đã tắt. Tìm kiếm địa điểm dùng OpenStreetMap, và Google nếu đã đặt khóa.', 'admin.trekApi.tagline': 'Chỉ mục địa điểm của riêng TREK. Tìm kiếm không cần khóa Google, không hạn mức, và không ai đếm lượt tìm của bạn.', 'admin.trekApi.factPlaces': diff --git a/shared/src/i18n/zh-TW/admin.ts b/shared/src/i18n/zh-TW/admin.ts index 1b1040c47..88501e474 100644 --- a/shared/src/i18n/zh-TW/admin.ts +++ b/shared/src/i18n/zh-TW/admin.ts @@ -130,10 +130,10 @@ const admin: TranslationStrings = { 'admin.googleCaveat.badge': '不建議', 'admin.googleCaveat.body': 'TREK 是開源軟體,在這件事上我們並不中立。在這個規模上,評分和一般店家的照片只有 Google 有,這就是壟斷。這個欄位存在是因為沒有替代品,而不是因為我們推薦。填了它,每次查詢都會送往 Google。', - 'admin.trekApi.badgeSoon': - '即將推出', - 'admin.trekApi.badgeActive': - '已啟用', + 'admin.trekApi.toggleLabel': + 'TREK 地點索引', + 'admin.trekApi.offNote': + '已關閉。地點搜尋使用 OpenStreetMap;若已設定金鑰,也會使用 Google。', 'admin.trekApi.tagline': 'TREK 自己的地點索引。不用 Google 金鑰,沒有配額,也沒有人統計你搜了什麼。', 'admin.trekApi.factPlaces': diff --git a/shared/src/i18n/zh/admin.ts b/shared/src/i18n/zh/admin.ts index 6b1d9f7a7..403d3da93 100644 --- a/shared/src/i18n/zh/admin.ts +++ b/shared/src/i18n/zh/admin.ts @@ -91,10 +91,10 @@ const admin: TranslationStrings = { 'admin.googleCaveat.badge': '不推荐', 'admin.googleCaveat.body': 'TREK 是开源软件,在这件事上我们并不中立。在这个规模上,评分和普通店铺的照片只有 Google 有,这就是垄断。这个输入框存在是因为没有替代品,而不是因为我们推荐。填了它,每次查询都会发往 Google。', - 'admin.trekApi.badgeSoon': - '即将推出', - 'admin.trekApi.badgeActive': - '已启用', + 'admin.trekApi.toggleLabel': + 'TREK 地点索引', + 'admin.trekApi.offNote': + '已关闭。地点搜索使用 OpenStreetMap;如果已配置密钥,也会使用 Google。', 'admin.trekApi.tagline': 'TREK 自己的地点索引。不用 Google 密钥,没有配额,也没有人统计你搜了什么。', 'admin.trekApi.factPlaces':