mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-11-13 15:51:00 +00:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 74abaa9d7c | |||
| 091505d17c |
@@ -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')
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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<string, string> = {
|
||||
'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');
|
||||
}
|
||||
|
||||
|
||||
@@ -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,39 @@ 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<unknown> }).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 () => {
|
||||
const refusedWith = (status: number) => {
|
||||
const refused = fakeRes({ ok: false, status, type: 'text/plain' });
|
||||
(refused as { json: () => Promise<unknown> }).json = async () => {
|
||||
throw new SyntaxError('Unexpected token A in JSON');
|
||||
};
|
||||
return refused;
|
||||
};
|
||||
vi.stubGlobal('fetch', vi.fn().mockResolvedValueOnce(refusedWith(401)).mockResolvedValueOnce(refusedWith(403)));
|
||||
const errors = [
|
||||
await svc.searchUnsplashPhotos('paris').catch((e: unknown) => e),
|
||||
await svc.searchUnsplashPhotos('paris').catch((e: unknown) => e),
|
||||
];
|
||||
for (const err of errors) {
|
||||
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 +164,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 () => {
|
||||
|
||||
@@ -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': 'يجب أن يكون تاريخ النهاية بعد البداية',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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': 'Η ημερομηνία λήξης πρέπει να είναι μετά την ημερομηνία έναρξης',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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': '終了日は開始日より後にしてください',
|
||||
|
||||
@@ -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': '종료일은 시작일 이후여야 합니다',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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': 'Дата окончания должна быть позже даты начала',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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': 'วันที่สิ้นสุดต้องอยู่หลังวันที่เริ่มต้น',
|
||||
|
||||
@@ -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ı',
|
||||
|
||||
@@ -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': 'Дата закінчення повинна бути пізніше дати початку',
|
||||
|
||||
@@ -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.',
|
||||
|
||||
@@ -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': '結束日期必須晚於開始日期',
|
||||
|
||||
@@ -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': '结束日期必须晚于开始日期',
|
||||
|
||||
Reference in New Issue
Block a user