From 091505d17c8fb7725dfef7dac278b6c02126a73c Mon Sep 17 00:00:00 2001 From: Maurice Date: Wed, 28 Oct 2026 14:54:45 +0100 Subject: [PATCH] fix(unsplash): tell a server Unsplash turns away that the free key helps Without an Access Key the image search goes through Unsplash's web endpoint, which refuses datacenter addresses with a 401 or 403 or answers with a challenge page. The search then said only "Unsplash search unavailable", although an admin can already enter a free key in the settings. Such a refusal now answers 502 with that hint and the code unsplash_key_needed, and the cover search shows it in the user's language. Closes #1449 --- .../Trips/useTripCoverPicker.test.ts | 10 ++++++ .../components/Trips/useTripCoverPicker.ts | 8 ++++- server/src/nest/unsplash/unsplash.service.ts | 18 +++++++++++ .../nest/unsplash/unsplash.service.test.ts | 31 +++++++++++++++++-- shared/src/i18n/ar/dashboard.ts | 2 ++ shared/src/i18n/az/dashboard.ts | 2 ++ shared/src/i18n/br/dashboard.ts | 2 ++ shared/src/i18n/ca/dashboard.ts | 2 ++ shared/src/i18n/cs/dashboard.ts | 2 ++ shared/src/i18n/de/dashboard.ts | 2 ++ shared/src/i18n/en/dashboard.ts | 2 ++ shared/src/i18n/es/dashboard.ts | 2 ++ shared/src/i18n/et/dashboard.ts | 2 ++ shared/src/i18n/fr/dashboard.ts | 2 ++ shared/src/i18n/gr/dashboard.ts | 2 ++ shared/src/i18n/hu/dashboard.ts | 2 ++ shared/src/i18n/id/dashboard.ts | 2 ++ shared/src/i18n/it/dashboard.ts | 2 ++ shared/src/i18n/ja/dashboard.ts | 2 ++ shared/src/i18n/ko/dashboard.ts | 2 ++ shared/src/i18n/nl/dashboard.ts | 2 ++ shared/src/i18n/pl/dashboard.ts | 2 ++ shared/src/i18n/ru/dashboard.ts | 2 ++ shared/src/i18n/sk/dashboard.ts | 2 ++ shared/src/i18n/sv/dashboard.ts | 2 ++ shared/src/i18n/th/dashboard.ts | 2 ++ shared/src/i18n/tr/dashboard.ts | 2 ++ shared/src/i18n/uk/dashboard.ts | 2 ++ shared/src/i18n/vi/dashboard.ts | 2 ++ shared/src/i18n/zh-TW/dashboard.ts | 2 ++ shared/src/i18n/zh/dashboard.ts | 2 ++ 31 files changed, 117 insertions(+), 4 deletions(-) diff --git a/client/src/components/Trips/useTripCoverPicker.test.ts b/client/src/components/Trips/useTripCoverPicker.test.ts index ad55f6ec8..aa8d91ee0 100644 --- a/client/src/components/Trips/useTripCoverPicker.test.ts +++ b/client/src/components/Trips/useTripCoverPicker.test.ts @@ -135,6 +135,16 @@ describe('useTripCoverPicker', () => { expect(result.current.searchError).toBe('offline'); }); + it('FE-COMP-TRIPCOVER-010b: a server Unsplash turned away says the admin can add the key (#1449)', async () => { + vi.spyOn(tripsApi, 'searchCoverImages').mockRejectedValueOnce({ + response: { status: 502, data: { error: 'Unsplash refused this server.', code: 'unsplash_key_needed' } }, + }); + const { result } = picker(); + act(() => result.current.setSearchQuery('Lisbon')); + await act(() => result.current.search()); + expect(result.current.searchError).toBe('dashboard.unsplashKeyNeeded'); + }); + it('FE-COMP-TRIPCOVER-011: only the latest search applies its results', async () => { let resolveFirst!: (v: { photos: (typeof photo)[] }) => void; vi.spyOn(tripsApi, 'searchCoverImages') diff --git a/client/src/components/Trips/useTripCoverPicker.ts b/client/src/components/Trips/useTripCoverPicker.ts index a3e98ba2b..00bedbcc7 100644 --- a/client/src/components/Trips/useTripCoverPicker.ts +++ b/client/src/components/Trips/useTripCoverPicker.ts @@ -4,6 +4,7 @@ import { tripsApi } from '../../api/client'; import { useTranslation } from '../../i18n'; import { isEffectivelyOffline } from '../../sync/networkMode'; import { getApiErrorMessage } from '../../types'; +import { getApiErrorCode } from '../../utils/apiError'; import { normalizeImageFile } from '../../utils/convertHeic'; import { refusedOffline } from '../../utils/needsConnection'; import { useToast } from '../shared/Toast'; @@ -149,7 +150,12 @@ export function useTripCoverPicker({ trip, title, onCoverUpdate, releasePreviewO if (photos.length === 0) setSearchError(t('dashboard.unsplashNoResults')); } catch (err: unknown) { if (seq !== coverSearchSeq.current) return; - setSearchError(getApiErrorMessage(err, t('dashboard.coverSearchError'))); + // #1449: Unsplash turns a datacenter address away until an admin enters the free key. + setSearchError( + getApiErrorCode(err) === 'unsplash_key_needed' + ? t('dashboard.unsplashKeyNeeded') + : getApiErrorMessage(err, t('dashboard.coverSearchError')) + ); } finally { if (seq === coverSearchSeq.current) setSearching(false); } diff --git a/server/src/nest/unsplash/unsplash.service.ts b/server/src/nest/unsplash/unsplash.service.ts index ca999602d..0dae8a9cd 100644 --- a/server/src/nest/unsplash/unsplash.service.ts +++ b/server/src/nest/unsplash/unsplash.service.ts @@ -41,6 +41,16 @@ const MAX_COVER_BYTES = 15 * 1024 * 1024; // Unsplash is a third party on the request path; safeFetch adds no deadline of // its own, so both the search and the cover download carry one. const UNSPLASH_TIMEOUT_MS = 10_000; +/** + * What a search is told when the keyless web endpoint turns this server away, as it + * does a datacenter (VPS) address: the free Access Key is the way out, and the admin + * who can enter it is the one to tell (#1449). The code lets the client say it in + * the user's language. + */ +const UNSPLASH_KEY_NEEDED = + 'Unsplash refused this server. An administrator can add a free Unsplash Access Key in Admin > Settings.'; +const UNSPLASH_KEY_NEEDED_CODE = 'unsplash_key_needed'; + const COVER_EXT_BY_TYPE: Record = { 'image/jpeg': '.jpg', 'image/png': '.png', @@ -120,10 +130,18 @@ export class UnsplashService { 'Sec-Fetch-Site': 'same-origin', }, }); + // Turned away without a key: a 401 or 403, or an ok answer that is a challenge page, not JSON. + const refused = () => + new DomainError(502, UNSPLASH_KEY_NEEDED, { + code: UNSPLASH_KEY_NEEDED_CODE, + details: { code: UNSPLASH_KEY_NEEDED_CODE }, + }); + if (!accessKey && (response.status === 401 || response.status === 403)) throw refused(); let data: UnsplashSearchResponse; try { data = (await response.json()) as UnsplashSearchResponse; } catch { + if (!accessKey && response.ok) throw refused(); throw new DomainError(response.ok ? 502 : response.status, 'Unsplash search unavailable'); } diff --git a/server/tests/unit/nest/unsplash/unsplash.service.test.ts b/server/tests/unit/nest/unsplash/unsplash.service.test.ts index 72127e859..18b0697b1 100644 --- a/server/tests/unit/nest/unsplash/unsplash.service.test.ts +++ b/server/tests/unit/nest/unsplash/unsplash.service.test.ts @@ -1,6 +1,7 @@ import type { AppSettingsRepository } from '../../../../src/db/repositories/AppSettings.repository'; import type { UsersRepository } from '../../../../src/db/repositories/Users.repository'; import { RuntimeEnvService } from '../../../../src/nest/app-config/runtime-env.service'; +import { DomainError } from '../../../../src/nest/common/domain-error'; import { UnsplashService } from '../../../../src/nest/unsplash/unsplash.service'; import { asLegacyResult } from '../../../helpers/domain-error'; import { makeStorageFixture } from '../../../helpers/storage-fixture'; @@ -40,6 +41,9 @@ const getUnsplashKey = svc.getUnsplashKey.bind(svc); const saveUnsplashCover = svc.saveUnsplashCover.bind(svc); const isUnsplashCoverUrl = svc.isUnsplashCoverUrl.bind(svc); +const KEY_NEEDED = + 'Unsplash refused this server. An administrator can add a free Unsplash Access Key in Admin > Settings.'; + const ORIGINAL_UNSPLASH_ENV = process.env.UNSPLASH_ACCESS_KEY; afterEach(() => { @@ -99,13 +103,33 @@ describe('unsplashService.searchUnsplashPhotos', () => { it('UNSPLASH-016: a 200 carrying unparseable JSON becomes a 502, not a crash', async () => { // The web endpoint answers 200 with an HTML challenge page when it decides the // caller is a datacenter (#1449), so a parse failure on an ok response is an - // upstream problem, not the caller's. + // upstream problem, not the caller's: without a key, the key is the way out. const bad = fakeRes({ ok: true, type: 'text/html' }); (bad as { json: () => Promise }).json = async () => { throw new SyntaxError('Unexpected token < in JSON'); }; vi.stubGlobal('fetch', vi.fn().mockResolvedValue(bad)); - expect(await searchUnsplashPhotos('paris')).toEqual({ error: 'Unsplash search unavailable', status: 502 }); + expect(await searchUnsplashPhotos('paris')).toMatchObject({ error: KEY_NEEDED, status: 502 }); + // With a key the official API answered, so no key is named. + expect(await searchUnsplashPhotos('paris', 9, 'key')).toEqual({ + error: 'Unsplash search unavailable', + status: 502, + }); + }); + + it('UNSPLASH-016b: a keyless search turned away with 401 or 403 says the Access Key is the way out (#1449)', async () => { + for (const status of [401, 403]) { + const refused = fakeRes({ ok: false, status, type: 'text/plain' }); + (refused as { json: () => Promise }).json = async () => { + throw new SyntaxError('Unexpected token A in JSON'); + }; + vi.stubGlobal('fetch', vi.fn().mockResolvedValue(refused)); + const err = await svc.searchUnsplashPhotos('paris').catch((e: unknown) => e); + expect(err).toBeInstanceOf(DomainError); + expect((err as DomainError).getStatus()).toBe(502); + expect(err).toMatchObject({ publicMessage: KEY_NEEDED, code: 'unsplash_key_needed' }); + expect((err as DomainError).toBody()).toEqual({ error: KEY_NEEDED, code: 'unsplash_key_needed' }); + } }); it('UNSPLASH-017: a failing response with unparseable JSON keeps its own status', async () => { @@ -134,7 +158,8 @@ describe('unsplashService.searchUnsplashPhotos', () => { fakeRes({ ok: false, status: 401, type: 'application/json', json: { error: 'Bad credentials' } }), ), ); - expect(await searchUnsplashPhotos('paris')).toEqual({ error: 'Bad credentials', status: 401 }); + // A key the official API refuses is named by Unsplash itself. + expect(await searchUnsplashPhotos('paris', 9, 'stale-key')).toEqual({ error: 'Bad credentials', status: 401 }); }); it('UNSPLASH-004: returns normalised photos on success and drops entries missing a url/thumb', async () => { diff --git a/shared/src/i18n/ar/dashboard.ts b/shared/src/i18n/ar/dashboard.ts index 7adabaef9..f05116584 100644 --- a/shared/src/i18n/ar/dashboard.ts +++ b/shared/src/i18n/ar/dashboard.ts @@ -124,6 +124,8 @@ const dashboard: TranslationStrings = { 'dashboard.unsplashQueryRequired': 'أدخل عبارة بحث', 'dashboard.unsplashNoResults': 'لم يتم العثور على صور', 'dashboard.coverSearchError': 'فشل البحث في Unsplash', + 'dashboard.unsplashKeyNeeded': + 'رفض Unsplash هذا الخادم. يمكن للمسؤول إضافة مفتاح Unsplash API مجاني في إعدادات المسؤول لتعمل عملية البحث عن الصور.', 'dashboard.useUnsplashPhoto': 'استخدم صورة من Unsplash بواسطة {photographer}', 'dashboard.titleRequired': 'العنوان مطلوب', 'dashboard.endDateError': 'يجب أن يكون تاريخ النهاية بعد البداية', diff --git a/shared/src/i18n/az/dashboard.ts b/shared/src/i18n/az/dashboard.ts index ea0a678f7..00e29d78e 100644 --- a/shared/src/i18n/az/dashboard.ts +++ b/shared/src/i18n/az/dashboard.ts @@ -111,6 +111,8 @@ const dashboard: TranslationStrings = { 'dashboard.unsplashQueryRequired': 'Axtarış sözü daxil edin', 'dashboard.unsplashNoResults': 'Şəkil tapılmadı', 'dashboard.coverSearchError': 'Unsplash-da axtarış aparmaq mümkün olmadı', + 'dashboard.unsplashKeyNeeded': + 'Unsplash bu serveri rədd etdi. Şəkil axtarışının işləməsi üçün administrator admin ayarlarında pulsuz Unsplash API açarı əlavə edə bilər.', 'dashboard.useUnsplashPhoto': '{photographer} tərəfindən çəkilmiş Unsplash şəklindən istifadə et', 'dashboard.titleRequired': 'Başlıq tələb olunur', 'dashboard.endDateError': 'Bitmə tarixi başlama tarixindən sonra olmalıdır', diff --git a/shared/src/i18n/br/dashboard.ts b/shared/src/i18n/br/dashboard.ts index 830d3eb80..4b2ce9c44 100644 --- a/shared/src/i18n/br/dashboard.ts +++ b/shared/src/i18n/br/dashboard.ts @@ -96,6 +96,8 @@ const dashboard: TranslationStrings = { 'dashboard.unsplashQueryRequired': 'Digite um termo de busca', 'dashboard.unsplashNoResults': 'Nenhuma imagem encontrada', 'dashboard.coverSearchError': 'Falha ao buscar no Unsplash', + 'dashboard.unsplashKeyNeeded': + 'O Unsplash recusou este servidor. Um administrador pode adicionar uma chave de API gratuita do Unsplash nas configurações de administração para a busca de imagens funcionar.', 'dashboard.useUnsplashPhoto': 'Usar foto do Unsplash por {photographer}', 'dashboard.titleRequired': 'O título é obrigatório', 'dashboard.endDateError': 'A data final deve ser depois da inicial', diff --git a/shared/src/i18n/ca/dashboard.ts b/shared/src/i18n/ca/dashboard.ts index f00a6e0be..131ed4d52 100644 --- a/shared/src/i18n/ca/dashboard.ts +++ b/shared/src/i18n/ca/dashboard.ts @@ -186,6 +186,8 @@ const dashboard: TranslationStrings = { 'dashboard.unsplashQueryRequired': 'Introdueix un terme de cerca', 'dashboard.unsplashNoResults': "No s'han trobat imatges", 'dashboard.coverSearchError': 'Error en cercar a Unsplash', + 'dashboard.unsplashKeyNeeded': + 'Unsplash ha rebutjat aquest servidor. Un administrador pot afegir una clau d’API gratuïta d’Unsplash a la configuració d’administració perquè funcioni la cerca d’imatges.', 'dashboard.useUnsplashPhoto': "Utilitzar foto d'Unsplash de {photographer}", 'dashboard.dayCountRequired': 'El nombre de dies és obligatori', 'dashboard.dateShiftTitle': "Nova data d'inici", diff --git a/shared/src/i18n/cs/dashboard.ts b/shared/src/i18n/cs/dashboard.ts index 641ecbb49..d5cdbffaa 100644 --- a/shared/src/i18n/cs/dashboard.ts +++ b/shared/src/i18n/cs/dashboard.ts @@ -103,6 +103,8 @@ const dashboard: TranslationStrings = { 'dashboard.unsplashQueryRequired': 'Zadejte hledaný výraz', 'dashboard.unsplashNoResults': 'Nebyly nalezeny žádné obrázky', 'dashboard.coverSearchError': 'Nepodařilo se vyhledat na Unsplash', + 'dashboard.unsplashKeyNeeded': + 'Unsplash tento server odmítl. Správce může v nastavení administrace přidat bezplatný klíč Unsplash API, aby vyhledávání obrázků fungovalo.', 'dashboard.useUnsplashPhoto': 'Použít fotku z Unsplash od {photographer}', 'dashboard.titleRequired': 'Název je povinný', 'dashboard.endDateError': 'Datum konce musí být po datu začátku', diff --git a/shared/src/i18n/de/dashboard.ts b/shared/src/i18n/de/dashboard.ts index 43910b0e3..efbc4646b 100644 --- a/shared/src/i18n/de/dashboard.ts +++ b/shared/src/i18n/de/dashboard.ts @@ -97,6 +97,8 @@ const dashboard: TranslationStrings = { 'dashboard.unsplashQueryRequired': 'Suchbegriff eingeben', 'dashboard.unsplashNoResults': 'Keine Bilder gefunden', 'dashboard.coverSearchError': 'Unsplash-Suche fehlgeschlagen', + 'dashboard.unsplashKeyNeeded': + 'Unsplash hat diesen Server abgewiesen. Ein Administrator kann in den Admin-Einstellungen einen kostenlosen Unsplash-API-Schlüssel eintragen, dann funktioniert die Bildsuche.', 'dashboard.useUnsplashPhoto': 'Unsplash-Foto von {photographer} verwenden', 'dashboard.titleRequired': 'Titel ist erforderlich', 'dashboard.endDateError': 'Enddatum muss nach dem Startdatum liegen', diff --git a/shared/src/i18n/en/dashboard.ts b/shared/src/i18n/en/dashboard.ts index b761e8c17..d18c3a80f 100644 --- a/shared/src/i18n/en/dashboard.ts +++ b/shared/src/i18n/en/dashboard.ts @@ -110,6 +110,8 @@ const dashboard: TranslationStrings = { 'dashboard.unsplashQueryRequired': 'Enter a search term', 'dashboard.unsplashNoResults': 'No images found', 'dashboard.coverSearchError': 'Failed to search Unsplash', + 'dashboard.unsplashKeyNeeded': + 'Unsplash turned this server away. An administrator can add a free Unsplash API Key in the admin settings to make image search work.', 'dashboard.useUnsplashPhoto': 'Use Unsplash photo by {photographer}', 'dashboard.titleRequired': 'Title is required', 'dashboard.endDateError': 'End date must be after start date', diff --git a/shared/src/i18n/es/dashboard.ts b/shared/src/i18n/es/dashboard.ts index 6fbe77f32..990e37fcf 100644 --- a/shared/src/i18n/es/dashboard.ts +++ b/shared/src/i18n/es/dashboard.ts @@ -96,6 +96,8 @@ const dashboard: TranslationStrings = { 'dashboard.unsplashQueryRequired': 'Introduce un término de búsqueda', 'dashboard.unsplashNoResults': 'No se encontraron imágenes', 'dashboard.coverSearchError': 'No se pudo buscar en Unsplash', + 'dashboard.unsplashKeyNeeded': + 'Unsplash ha rechazado este servidor. Un administrador puede añadir una clave de API gratuita de Unsplash en los ajustes de administración para que funcione la búsqueda de imágenes.', 'dashboard.useUnsplashPhoto': 'Usar foto de Unsplash de {photographer}', 'dashboard.titleRequired': 'El título es obligatorio', 'dashboard.endDateError': 'La fecha de fin debe ser posterior a la de inicio', diff --git a/shared/src/i18n/et/dashboard.ts b/shared/src/i18n/et/dashboard.ts index 0ef8f042b..e7cdd7385 100644 --- a/shared/src/i18n/et/dashboard.ts +++ b/shared/src/i18n/et/dashboard.ts @@ -110,6 +110,8 @@ const dashboard: TranslationStrings = { 'dashboard.unsplashQueryRequired': 'Sisesta otsingusõna', 'dashboard.unsplashNoResults': 'Pilte ei leitud', 'dashboard.coverSearchError': 'Unsplashi otsing ebaõnnestus', + 'dashboard.unsplashKeyNeeded': + 'Unsplash lükkas selle serveri tagasi. Administraator saab halduse seadetes lisada tasuta Unsplash API võtme, et pildiotsing töötaks.', 'dashboard.useUnsplashPhoto': 'Kasuta Unsplashi fotot autorilt {photographer}', 'dashboard.titleRequired': 'Pealkiri on kohustuslik', 'dashboard.endDateError': 'Lõppkuupäev peab olema alguskuupäevast hilisem', diff --git a/shared/src/i18n/fr/dashboard.ts b/shared/src/i18n/fr/dashboard.ts index 9822c4462..8b9b4695d 100644 --- a/shared/src/i18n/fr/dashboard.ts +++ b/shared/src/i18n/fr/dashboard.ts @@ -98,6 +98,8 @@ const dashboard: TranslationStrings = { 'dashboard.unsplashQueryRequired': 'Saisissez un terme de recherche', 'dashboard.unsplashNoResults': 'Aucune image trouvée', 'dashboard.coverSearchError': 'Échec de la recherche Unsplash', + 'dashboard.unsplashKeyNeeded': + 'Unsplash a refusé ce serveur. Un administrateur peut ajouter une clé API Unsplash gratuite dans les paramètres d’administration pour que la recherche d’images fonctionne.', 'dashboard.useUnsplashPhoto': 'Utiliser la photo Unsplash de {photographer}', 'dashboard.titleRequired': 'Le titre est obligatoire', 'dashboard.endDateError': 'La date de fin doit être postérieure à la date de début', diff --git a/shared/src/i18n/gr/dashboard.ts b/shared/src/i18n/gr/dashboard.ts index 82fc7f116..610fd917d 100644 --- a/shared/src/i18n/gr/dashboard.ts +++ b/shared/src/i18n/gr/dashboard.ts @@ -108,6 +108,8 @@ const dashboard: TranslationStrings = { 'dashboard.unsplashQueryRequired': 'Πληκτρολογήστε όρο αναζήτησης', 'dashboard.unsplashNoResults': 'Δεν βρέθηκαν εικόνες', 'dashboard.coverSearchError': 'Αποτυχία αναζήτησης στο Unsplash', + 'dashboard.unsplashKeyNeeded': + 'Το Unsplash απέρριψε αυτόν τον διακομιστή. Ένας διαχειριστής μπορεί να προσθέσει ένα δωρεάν κλειδί Unsplash API στις ρυθμίσεις διαχείρισης ώστε να λειτουργεί η αναζήτηση εικόνων.', 'dashboard.useUnsplashPhoto': 'Χρήση φωτογραφίας Unsplash από {photographer}', 'dashboard.titleRequired': 'Ο τίτλος είναι υποχρεωτικός', 'dashboard.endDateError': 'Η ημερομηνία λήξης πρέπει να είναι μετά την ημερομηνία έναρξης', diff --git a/shared/src/i18n/hu/dashboard.ts b/shared/src/i18n/hu/dashboard.ts index 1dd57e3aa..cf880d72e 100644 --- a/shared/src/i18n/hu/dashboard.ts +++ b/shared/src/i18n/hu/dashboard.ts @@ -98,6 +98,8 @@ const dashboard: TranslationStrings = { 'dashboard.unsplashQueryRequired': 'Adj meg egy keresési kifejezést', 'dashboard.unsplashNoResults': 'Nem található kép', 'dashboard.coverSearchError': 'Nem sikerült keresni az Unsplashen', + 'dashboard.unsplashKeyNeeded': + 'Az Unsplash elutasította ezt a szervert. Egy rendszergazda az adminisztrációs beállításokban megadhat egy ingyenes Unsplash API-kulcsot, és akkor működik a képkeresés.', 'dashboard.useUnsplashPhoto': '{photographer} Unsplash-fotójának használata', 'dashboard.titleRequired': 'A cím megadása kötelező', 'dashboard.endDateError': 'A záró dátumnak a kezdő dátum után kell lennie', diff --git a/shared/src/i18n/id/dashboard.ts b/shared/src/i18n/id/dashboard.ts index 3ef9bca07..b6eab2307 100644 --- a/shared/src/i18n/id/dashboard.ts +++ b/shared/src/i18n/id/dashboard.ts @@ -89,6 +89,8 @@ const dashboard: TranslationStrings = { 'dashboard.unsplashQueryRequired': 'Masukkan istilah pencarian', 'dashboard.unsplashNoResults': 'Tidak ada gambar ditemukan', 'dashboard.coverSearchError': 'Gagal mencari di Unsplash', + 'dashboard.unsplashKeyNeeded': + 'Unsplash menolak server ini. Administrator dapat menambahkan kunci API Unsplash gratis di pengaturan admin agar pencarian gambar berfungsi.', 'dashboard.useUnsplashPhoto': 'Gunakan foto Unsplash oleh {photographer}', 'dashboard.titleRequired': 'Judul wajib diisi', 'dashboard.endDateError': 'Tanggal selesai harus setelah tanggal mulai', diff --git a/shared/src/i18n/it/dashboard.ts b/shared/src/i18n/it/dashboard.ts index 91272d49a..d45d90d14 100644 --- a/shared/src/i18n/it/dashboard.ts +++ b/shared/src/i18n/it/dashboard.ts @@ -98,6 +98,8 @@ const dashboard: TranslationStrings = { 'dashboard.unsplashQueryRequired': 'Inserisci un termine di ricerca', 'dashboard.unsplashNoResults': 'Nessuna immagine trovata', 'dashboard.coverSearchError': 'Ricerca su Unsplash non riuscita', + 'dashboard.unsplashKeyNeeded': + 'Unsplash ha rifiutato questo server. Un amministratore può aggiungere una chiave API Unsplash gratuita nelle impostazioni di amministrazione per far funzionare la ricerca di immagini.', 'dashboard.useUnsplashPhoto': 'Usa foto Unsplash di {photographer}', 'dashboard.titleRequired': 'Il titolo è obbligatorio', 'dashboard.endDateError': 'La data di fine deve essere successiva alla data di inizio', diff --git a/shared/src/i18n/ja/dashboard.ts b/shared/src/i18n/ja/dashboard.ts index e9a010257..0713baadb 100644 --- a/shared/src/i18n/ja/dashboard.ts +++ b/shared/src/i18n/ja/dashboard.ts @@ -104,6 +104,8 @@ const dashboard: TranslationStrings = { 'dashboard.unsplashQueryRequired': '検索語を入力してください', 'dashboard.unsplashNoResults': '画像が見つかりません', 'dashboard.coverSearchError': 'Unsplashの検索に失敗しました', + 'dashboard.unsplashKeyNeeded': + 'Unsplash がこのサーバーを拒否しました。管理者が管理設定で無料の Unsplash API キーを追加すると、画像検索が使えるようになります。', 'dashboard.useUnsplashPhoto': '{photographer}さんのUnsplash写真を使用', 'dashboard.titleRequired': 'タイトルは必須です', 'dashboard.endDateError': '終了日は開始日より後にしてください', diff --git a/shared/src/i18n/ko/dashboard.ts b/shared/src/i18n/ko/dashboard.ts index d359ba24e..70007ff03 100644 --- a/shared/src/i18n/ko/dashboard.ts +++ b/shared/src/i18n/ko/dashboard.ts @@ -104,6 +104,8 @@ const dashboard: TranslationStrings = { 'dashboard.unsplashQueryRequired': '검색어를 입력하세요', 'dashboard.unsplashNoResults': '이미지를 찾을 수 없습니다', 'dashboard.coverSearchError': 'Unsplash 검색에 실패했습니다', + 'dashboard.unsplashKeyNeeded': + 'Unsplash가 이 서버를 거부했습니다. 관리자가 관리자 설정에서 무료 Unsplash API 키를 추가하면 이미지 검색이 작동합니다.', 'dashboard.useUnsplashPhoto': '{photographer}님의 Unsplash 사진 사용', 'dashboard.titleRequired': '제목을 입력하세요', 'dashboard.endDateError': '종료일은 시작일 이후여야 합니다', diff --git a/shared/src/i18n/nl/dashboard.ts b/shared/src/i18n/nl/dashboard.ts index 1ff184c3b..e9229d65a 100644 --- a/shared/src/i18n/nl/dashboard.ts +++ b/shared/src/i18n/nl/dashboard.ts @@ -96,6 +96,8 @@ const dashboard: TranslationStrings = { 'dashboard.unsplashQueryRequired': 'Voer een zoekterm in', 'dashboard.unsplashNoResults': 'Geen afbeeldingen gevonden', 'dashboard.coverSearchError': 'Zoeken op Unsplash mislukt', + 'dashboard.unsplashKeyNeeded': + 'Unsplash heeft deze server geweigerd. Een beheerder kan in de beheerinstellingen een gratis Unsplash-API-sleutel toevoegen, dan werkt het zoeken naar afbeeldingen.', 'dashboard.useUnsplashPhoto': 'Unsplash-foto van {photographer} gebruiken', 'dashboard.titleRequired': 'Titel is verplicht', 'dashboard.endDateError': 'Einddatum moet na de startdatum liggen', diff --git a/shared/src/i18n/pl/dashboard.ts b/shared/src/i18n/pl/dashboard.ts index 3ffc07ca6..6a9667c05 100644 --- a/shared/src/i18n/pl/dashboard.ts +++ b/shared/src/i18n/pl/dashboard.ts @@ -102,6 +102,8 @@ const dashboard: TranslationStrings = { 'dashboard.unsplashQueryRequired': 'Wpisz szukaną frazę', 'dashboard.unsplashNoResults': 'Nie znaleziono obrazów', 'dashboard.coverSearchError': 'Nie udało się wyszukać w Unsplash', + 'dashboard.unsplashKeyNeeded': + 'Unsplash odrzucił ten serwer. Administrator może dodać darmowy klucz Unsplash API w ustawieniach administracyjnych, aby wyszukiwanie obrazów działało.', 'dashboard.useUnsplashPhoto': 'Użyj zdjęcia z Unsplash autorstwa {photographer}', 'dashboard.titleRequired': 'Nazwa podróży jest wymagana', 'dashboard.endDateError': 'Data zakończenia musi być po dacie rozpoczęcia', diff --git a/shared/src/i18n/ru/dashboard.ts b/shared/src/i18n/ru/dashboard.ts index 4942912d7..0ae8650c8 100644 --- a/shared/src/i18n/ru/dashboard.ts +++ b/shared/src/i18n/ru/dashboard.ts @@ -110,6 +110,8 @@ const dashboard: TranslationStrings = { 'dashboard.unsplashQueryRequired': 'Введите поисковый запрос', 'dashboard.unsplashNoResults': 'Изображения не найдены', 'dashboard.coverSearchError': 'Не удалось выполнить поиск в Unsplash', + 'dashboard.unsplashKeyNeeded': + 'Unsplash отклонил этот сервер. Администратор может добавить бесплатный ключ Unsplash API в настройках администрирования, чтобы заработал поиск изображений.', 'dashboard.useUnsplashPhoto': 'Использовать фото Unsplash от {photographer}', 'dashboard.titleRequired': 'Название обязательно', 'dashboard.endDateError': 'Дата окончания должна быть позже даты начала', diff --git a/shared/src/i18n/sk/dashboard.ts b/shared/src/i18n/sk/dashboard.ts index de9c773ff..fea4201b7 100644 --- a/shared/src/i18n/sk/dashboard.ts +++ b/shared/src/i18n/sk/dashboard.ts @@ -117,6 +117,8 @@ const dashboard: TranslationStrings = { 'dashboard.unsplashQueryRequired': 'Zadajte hľadaný výraz', 'dashboard.unsplashNoResults': 'Nenašli sa žiadne obrázky', 'dashboard.coverSearchError': 'Nepodarilo sa vyhľadať na Unsplash', + 'dashboard.unsplashKeyNeeded': + 'Unsplash tento server odmietol. Správca môže v nastaveniach administrácie pridať bezplatný kľúč Unsplash API, aby vyhľadávanie obrázkov fungovalo.', 'dashboard.useUnsplashPhoto': 'Použiť fotku z Unsplash od {photographer}', 'dashboard.titleRequired': 'Názov je povinný', 'dashboard.endDateError': 'Dátum konca musí byť po dátume začiatku', diff --git a/shared/src/i18n/sv/dashboard.ts b/shared/src/i18n/sv/dashboard.ts index 3eee657a3..7f4e1e1f3 100644 --- a/shared/src/i18n/sv/dashboard.ts +++ b/shared/src/i18n/sv/dashboard.ts @@ -110,6 +110,8 @@ const dashboard: TranslationStrings = { 'dashboard.unsplashQueryRequired': 'Ange en sökterm', 'dashboard.unsplashNoResults': 'Inga bilder hittades', 'dashboard.coverSearchError': 'Det gick inte att söka på Unsplash', + 'dashboard.unsplashKeyNeeded': + 'Unsplash avvisade den här servern. En administratör kan lägga till en gratis Unsplash-API-nyckel i administratörsinställningarna så att bildsökningen fungerar.', 'dashboard.useUnsplashPhoto': 'Använd Unsplash-foto av {photographer}', 'dashboard.titleRequired': 'Titel är obligatoriskt', 'dashboard.endDateError': 'Slutdatumet måste ligga efter startdatumet', diff --git a/shared/src/i18n/th/dashboard.ts b/shared/src/i18n/th/dashboard.ts index b482f74ba..dbd33a6a4 100644 --- a/shared/src/i18n/th/dashboard.ts +++ b/shared/src/i18n/th/dashboard.ts @@ -103,6 +103,8 @@ const dashboard: TranslationStrings = { 'dashboard.unsplashQueryRequired': 'ป้อนคำค้นหา', 'dashboard.unsplashNoResults': 'ไม่พบภาพ', 'dashboard.coverSearchError': 'ไม่สามารถค้นหา Unsplash', + 'dashboard.unsplashKeyNeeded': + 'Unsplash ปฏิเสธเซิร์ฟเวอร์นี้ ผู้ดูแลระบบสามารถเพิ่มคีย์ Unsplash API ฟรีในการตั้งค่าผู้ดูแลระบบเพื่อให้การค้นหารูปภาพใช้งานได้', 'dashboard.useUnsplashPhoto': 'ใช้รูปภาพ Unsplash โดย {photographer}', 'dashboard.titleRequired': 'ต้องระบุชื่อ', 'dashboard.endDateError': 'วันที่สิ้นสุดต้องอยู่หลังวันที่เริ่มต้น', diff --git a/shared/src/i18n/tr/dashboard.ts b/shared/src/i18n/tr/dashboard.ts index 426a90025..6243c2079 100644 --- a/shared/src/i18n/tr/dashboard.ts +++ b/shared/src/i18n/tr/dashboard.ts @@ -110,6 +110,8 @@ const dashboard: TranslationStrings = { 'dashboard.unsplashQueryRequired': 'Bir arama terimi girin', 'dashboard.unsplashNoResults': 'Görsel bulunamadı', 'dashboard.coverSearchError': 'Unsplash araması başarısız oldu', + 'dashboard.unsplashKeyNeeded': + 'Unsplash bu sunucuyu reddetti. Görsel aramanın çalışması için bir yönetici, yönetici ayarlarına ücretsiz bir Unsplash API anahtarı ekleyebilir.', 'dashboard.useUnsplashPhoto': '{photographer} tarafından çekilen Unsplash fotoğrafını kullan', 'dashboard.titleRequired': 'Başlık gerekli', 'dashboard.endDateError': 'Bitiş tarihi başlangıçtan sonra olmalı', diff --git a/shared/src/i18n/uk/dashboard.ts b/shared/src/i18n/uk/dashboard.ts index 71b17154f..c4f50e2b8 100644 --- a/shared/src/i18n/uk/dashboard.ts +++ b/shared/src/i18n/uk/dashboard.ts @@ -126,6 +126,8 @@ const dashboard: TranslationStrings = { 'dashboard.unsplashQueryRequired': 'Введіть пошуковий запит', 'dashboard.unsplashNoResults': 'Зображень не знайдено', 'dashboard.coverSearchError': 'Не вдалося виконати пошук в Unsplash', + 'dashboard.unsplashKeyNeeded': + 'Unsplash відхилив цей сервер. Адміністратор може додати безкоштовний ключ Unsplash API в налаштуваннях адміністрування, щоб запрацював пошук зображень.', 'dashboard.useUnsplashPhoto': 'Використати фото Unsplash від {photographer}', 'dashboard.titleRequired': "Назва обов'язкова", 'dashboard.endDateError': 'Дата закінчення повинна бути пізніше дати початку', diff --git a/shared/src/i18n/vi/dashboard.ts b/shared/src/i18n/vi/dashboard.ts index df12c6b2f..58a321812 100644 --- a/shared/src/i18n/vi/dashboard.ts +++ b/shared/src/i18n/vi/dashboard.ts @@ -203,6 +203,8 @@ const dashboard: TranslationStrings = { 'dashboard.unsplashQueryRequired': 'Nhập từ khóa tìm kiếm', 'dashboard.unsplashNoResults': 'Không tìm thấy ảnh nào', 'dashboard.coverSearchError': 'Tìm kiếm trên Unsplash không thành công', + 'dashboard.unsplashKeyNeeded': + 'Unsplash đã từ chối máy chủ này. Quản trị viên có thể thêm khóa API Unsplash miễn phí trong cài đặt quản trị để tìm kiếm hình ảnh hoạt động.', 'dashboard.useUnsplashPhoto': 'Dùng ảnh Unsplash của {photographer}', 'dashboard.tripChangeNeedsConnection': 'Đổi ngày, số ngày, tiền tệ hoặc ảnh bìa cần có kết nối. Tên, mô tả và lời nhắc có thể đổi khi ngoại tuyến.', diff --git a/shared/src/i18n/zh-TW/dashboard.ts b/shared/src/i18n/zh-TW/dashboard.ts index 966ee9ee0..11c11f46a 100644 --- a/shared/src/i18n/zh-TW/dashboard.ts +++ b/shared/src/i18n/zh-TW/dashboard.ts @@ -89,6 +89,8 @@ const dashboard: TranslationStrings = { 'dashboard.unsplashQueryRequired': '請輸入搜尋詞', 'dashboard.unsplashNoResults': '找不到圖片', 'dashboard.coverSearchError': 'Unsplash 搜尋失敗', + 'dashboard.unsplashKeyNeeded': + 'Unsplash 拒絕了此伺服器。管理員可以在管理設定中新增免費的 Unsplash API 金鑰,以便使用圖片搜尋。', 'dashboard.useUnsplashPhoto': '使用 {photographer} 的 Unsplash 照片', 'dashboard.titleRequired': '標題為必填項', 'dashboard.endDateError': '結束日期必須晚於開始日期', diff --git a/shared/src/i18n/zh/dashboard.ts b/shared/src/i18n/zh/dashboard.ts index d7ca2d0ac..942e135a7 100644 --- a/shared/src/i18n/zh/dashboard.ts +++ b/shared/src/i18n/zh/dashboard.ts @@ -89,6 +89,8 @@ const dashboard: TranslationStrings = { 'dashboard.unsplashQueryRequired': '请输入搜索词', 'dashboard.unsplashNoResults': '未找到图片', 'dashboard.coverSearchError': 'Unsplash 搜索失败', + 'dashboard.unsplashKeyNeeded': + 'Unsplash 拒绝了此服务器。管理员可以在管理设置中添加免费的 Unsplash API 密钥,以便使用图片搜索。', 'dashboard.useUnsplashPhoto': '使用 {photographer} 的 Unsplash 照片', 'dashboard.titleRequired': '标题为必填项', 'dashboard.endDateError': '结束日期必须晚于开始日期',