feat(media hosting): disable unreachable providers

This commit is contained in:
Tommaso Casaburi
2026-05-01 22:52:55 +07:00
parent 78e5d58d08
commit 7e1513b688
45 changed files with 596 additions and 130 deletions
+3 -2
View File
@@ -256,7 +256,7 @@
"upload_failed_all_providers": "فشل جميع مزودي الخدمة",
"upload_failed_preferred_guidance": "اختر مزود خدمة مختلفًا أو اضبط على عشوائي.",
"media_hosting_random": "عشوائي",
"media_hosting_preferred": "مزود مفضل",
"media_hosting_preferred": "اختر مزود الخدمة",
"media_hosting_preferred_provider_label": "اختر مزود الخدمة",
"media_hosting_preferred_failure_guidance": "إذا فشل مزودك المفضل، سيجرب التطبيق مزودين آخرين (وضع عشوائي) أو يعرض خطأ (وضع مفضل).",
"media_hosting_all_providers_failed_guidance": "فشل جميع مزودي التحميل. حاول مرة أخرى أو ألصق رابطًا يدويًا.",
@@ -364,5 +364,6 @@
"more_threads_last_week": "{{count}} مواضيع منذ {{currentTimeFilterName}} — [<1>عرض المزيد من المواضيع من الأسبوع الماضي</1>]",
"more_threads_last_month": "{{count}} مواضيع منذ {{currentTimeFilterName}} — [<1>عرض المزيد من المواضيع من الشهر الماضي</1>]",
"more_threads_last_year": "{{count}} مواضيع منذ {{currentTimeFilterName}} — [<1>عرض المزيد من المواضيع من السنة الماضية</1>]",
"iframe_challenge_confirm": "{{board}} يريد فتح {{site}}.\n\nبالنسبة إلى {{publicationType}}: {{excerpt}}"
"iframe_challenge_confirm": "{{board}} يريد فتح {{site}}.\n\nبالنسبة إلى {{publicationType}}: {{excerpt}}",
"media_hosting_provider_unavailable": "Unavailable from this network. Try another network or a VPN."
}
+3 -2
View File
@@ -256,7 +256,7 @@
"upload_failed_all_providers": "সমস্ত প্রদানকারী ব্যর্থ হয়েছে",
"upload_failed_preferred_guidance": "একটি ভিন্ন প্রদানকারী নির্বাচন করুন বা র‍্যান্ডমে সেট করুন।",
"media_hosting_random": "এলোমেলো",
"media_hosting_preferred": "পছন্দের প্রদানকারী",
"media_hosting_preferred": "প্রদানকারী নির্বাচন করুন",
"media_hosting_preferred_provider_label": "প্রদানকারী নির্বাচন করুন",
"media_hosting_preferred_failure_guidance": "যদি আপনার পছন্দের প্রদানকারী ব্যর্থ হয়, অ্যাপটি অন্যান্য প্রদানকারীদের চেষ্টা করবে (এলোমেলো মোড) বা একটি ত্রুটি দেখাবে (পছন্দের মোড)।",
"media_hosting_all_providers_failed_guidance": "সমস্ত আপলোড প্রদানকারী ব্যর্থ। আবার চেষ্টা করুন বা লিঙ্ক ম্যানুয়ালি পেস্ট করুন।",
@@ -364,5 +364,6 @@
"more_threads_last_week": "{{count}} থ্রেড {{currentTimeFilterName}} থেকে — [<1>গত সপ্তাহের আরও থ্রেড দেখান</1>]",
"more_threads_last_month": "{{count}} থ্রেড {{currentTimeFilterName}} থেকে — [<1>গত মাসের আরও থ্রেড দেখান</1>]",
"more_threads_last_year": "{{count}} থ্রেড {{currentTimeFilterName}} থেকে — [<1>গত বছরের আরও থ্রেড দেখান</1>]",
"iframe_challenge_confirm": "{{board}} {{site}} খুলতে চায়।\n\n{{publicationType}}-এর জন্য: {{excerpt}}"
"iframe_challenge_confirm": "{{board}} {{site}} খুলতে চায়।\n\n{{publicationType}}-এর জন্য: {{excerpt}}",
"media_hosting_provider_unavailable": "Unavailable from this network. Try another network or a VPN."
}
+3 -2
View File
@@ -256,7 +256,7 @@
"upload_failed_all_providers": "Všichni poskytovatelé selhali",
"upload_failed_preferred_guidance": "Zvolte jiného poskytovatele nebo nastavte náhodné.",
"media_hosting_random": "Náhodné",
"media_hosting_preferred": "Upřednostňovaný poskytovatel",
"media_hosting_preferred": "Zvolit poskytovatele",
"media_hosting_preferred_provider_label": "Zvolit poskytovatele",
"media_hosting_preferred_failure_guidance": "Pokud váš preferovaný poskytovatel selže, aplikace vyzkouší další poskytovatele (režim náhodný) nebo zobrazí chybu (režim preferovaný).",
"media_hosting_all_providers_failed_guidance": "Všichni poskytovatelé nahrávání selhali. Zkuste to znovu nebo vložte odkaz ručně.",
@@ -364,5 +364,6 @@
"more_threads_last_week": "{{count}} vláken od {{currentTimeFilterName}} — [<1>Ukázat více vláken z minulého týdne</1>]",
"more_threads_last_month": "{{count}} příspěvků od {{currentTimeFilterName}} — [<1>Zobrazit více příspěvků z minulého měsíce</1>]",
"more_threads_last_year": "{{count}} příspěvků od {{currentTimeFilterName}} — [<1>Zobrazit více příspěvků z minulého roku</1>]",
"iframe_challenge_confirm": "{{board}} chce otevřít {{site}}.\n\nPro {{publicationType}}: {{excerpt}}"
"iframe_challenge_confirm": "{{board}} chce otevřít {{site}}.\n\nPro {{publicationType}}: {{excerpt}}",
"media_hosting_provider_unavailable": "Unavailable from this network. Try another network or a VPN."
}
+3 -2
View File
@@ -256,7 +256,7 @@
"upload_failed_all_providers": "Alle udbydere mislykkedes",
"upload_failed_preferred_guidance": "Vælg en anden udbyder eller sæt til tilfældig.",
"media_hosting_random": "Tilfældig",
"media_hosting_preferred": "Foretrukket udbyder",
"media_hosting_preferred": "Vælg udbyder",
"media_hosting_preferred_provider_label": "Vælg udbyder",
"media_hosting_preferred_failure_guidance": "Hvis din foretrukne udbyder fejler, vil appen prøve andre udbydere (tilfældig tilstand) eller vise en fejl (foretrukket tilstand).",
"media_hosting_all_providers_failed_guidance": "Alle upload-udbydere mislykkedes. Prøv igen eller indsæt et link manuelt.",
@@ -364,5 +364,6 @@
"more_threads_last_week": "{{count}} tråde siden {{currentTimeFilterName}} — [<1>Vis flere tråde fra sidste uge</1>]",
"more_threads_last_month": "{{count}} tråde siden {{currentTimeFilterName}} — [<1>Vis flere tråde fra sidste måned</1>]",
"more_threads_last_year": "{{count}} tråde siden {{currentTimeFilterName}} — [<1>Vis flere tråde fra sidste år</1>]",
"iframe_challenge_confirm": "{{board}} vil åbne {{site}}.\n\nFor {{publicationType}}: {{excerpt}}"
"iframe_challenge_confirm": "{{board}} vil åbne {{site}}.\n\nFor {{publicationType}}: {{excerpt}}",
"media_hosting_provider_unavailable": "Unavailable from this network. Try another network or a VPN."
}
+3 -2
View File
@@ -256,7 +256,7 @@
"upload_failed_all_providers": "Alle Anbieter sind fehlgeschlagen",
"upload_failed_preferred_guidance": "Wählen Sie einen anderen Anbieter oder setzen Sie auf Zufall.",
"media_hosting_random": "Zufällig",
"media_hosting_preferred": "Bevorzugter Anbieter",
"media_hosting_preferred": "Anbieter wählen",
"media_hosting_preferred_provider_label": "Anbieter wählen",
"media_hosting_preferred_failure_guidance": "Wenn Ihr bevorzugter Anbieter fehlschlägt, versucht die App andere Anbieter (Zufallsmodus) oder zeigt einen Fehler an (Bevorzugter Modus).",
"media_hosting_all_providers_failed_guidance": "Alle Upload-Anbieter sind fehlgeschlagen. Versuchen Sie es erneut oder fügen Sie einen Link manuell ein.",
@@ -364,5 +364,6 @@
"more_threads_last_week": "{{count}} Threads seit {{currentTimeFilterName}} — [<1>Mehr Threads aus der letzten Woche anzeigen</1>]",
"more_threads_last_month": "{{count}} Threads seit {{currentTimeFilterName}} — [<1>Mehr Threads vom letzten Monat anzeigen</1>]",
"more_threads_last_year": "{{count}} Threads seit {{currentTimeFilterName}} — [<1>Mehr Threads vom letzten Jahr anzeigen</1>]",
"iframe_challenge_confirm": "{{board}} möchte {{site}} öffnen.\n\nFür {{publicationType}}: {{excerpt}}"
"iframe_challenge_confirm": "{{board}} möchte {{site}} öffnen.\n\nFür {{publicationType}}: {{excerpt}}",
"media_hosting_provider_unavailable": "Unavailable from this network. Try another network or a VPN."
}
+3 -2
View File
@@ -256,7 +256,7 @@
"upload_failed_all_providers": "Όλοι οι πάροχοι απέτυχαν",
"upload_failed_preferred_guidance": "Επιλέξτε διαφορετικό πάροχο ή ορίστε τυχαιότητα.",
"media_hosting_random": "Τυχαίο",
"media_hosting_preferred": "Προτιμώμενος πάροχος",
"media_hosting_preferred": "Επιλέξτε πάροχο",
"media_hosting_preferred_provider_label": "Επιλέξτε πάροχο",
"media_hosting_preferred_failure_guidance": "Αν ο προτιμώμενος πάροχος σας αποτύχει, η εφαρμογή θα δοκιμάσει άλλους πάροχους (τυχαία λειτουργία) ή θα εμφανίσει σφάλμα (προτιμώμενη λειτουργία).",
"media_hosting_all_providers_failed_guidance": "Όλοι οι πάροχοι μεταφόρτωσης απέτυχαν. Δοκιμάστε ξανά ή επικολλήστε έναν σύνδεσμο χειροκίνητα.",
@@ -364,5 +364,6 @@
"more_threads_last_week": "{{count}} θέματα από {{currentTimeFilterName}} — [<1>Εμφάνιση περισσότερων θεμάτων από την περασμένη εβδομάδα</1>]",
"more_threads_last_month": "{{count}} νήματα από {{currentTimeFilterName}} — [<1>Προβολή περισσότερων νημάτων από τον τελευταίο μήνα</1>]",
"more_threads_last_year": "{{count}} νήματα από {{currentTimeFilterName}} — [<1>Προβολή περισσότερων νημάτων από το περασμένο έτος</1>]",
"iframe_challenge_confirm": "Το {{board}} θέλει να ανοίξει το {{site}}.\n\nΓια {{publicationType}}: {{excerpt}}"
"iframe_challenge_confirm": "Το {{board}} θέλει να ανοίξει το {{site}}.\n\nΓια {{publicationType}}: {{excerpt}}",
"media_hosting_provider_unavailable": "Unavailable from this network. Try another network or a VPN."
}
+3 -2
View File
@@ -276,7 +276,7 @@
"upload_failed_all_providers": "All providers failed",
"upload_failed_preferred_guidance": "Choose a different provider or set to random.",
"media_hosting_random": "Random",
"media_hosting_preferred": "Preferred provider",
"media_hosting_preferred": "Choose provider",
"media_hosting_preferred_provider_label": "Choose provider",
"media_hosting_preferred_failure_guidance": "If your preferred provider fails, the app will try other providers (Random mode) or show an error (Preferred mode).",
"media_hosting_all_providers_failed_guidance": "All upload providers failed. Try again or paste a link manually.",
@@ -364,5 +364,6 @@
"more_threads_last_week": "{{count}} threads since {{currentTimeFilterName}} — [<1>Show More Threads from Last Week</1>]",
"more_threads_last_month": "{{count}} threads since {{currentTimeFilterName}} — [<1>Show More Threads from Last Month</1>]",
"more_threads_last_year": "{{count}} threads since {{currentTimeFilterName}} — [<1>Show More Threads from Last Year</1>]",
"iframe_challenge_confirm": "{{board}} wants to open {{site}}.\n\nFor {{publicationType}}: {{excerpt}}"
"iframe_challenge_confirm": "{{board}} wants to open {{site}}.\n\nFor {{publicationType}}: {{excerpt}}",
"media_hosting_provider_unavailable": "Unavailable from this network. Try another network or a VPN."
}
+3 -2
View File
@@ -256,7 +256,7 @@
"upload_failed_all_providers": "Todos los proveedores fallaron",
"upload_failed_preferred_guidance": "Elige un proveedor diferente o configura en aleatorio.",
"media_hosting_random": "Aleatorio",
"media_hosting_preferred": "Proveedor preferido",
"media_hosting_preferred": "Elegir proveedor",
"media_hosting_preferred_provider_label": "Elegir proveedor",
"media_hosting_preferred_failure_guidance": "Si tu proveedor preferido falla, la app intentará otros proveedores (modo aleatorio) o mostrará un error (modo preferido).",
"media_hosting_all_providers_failed_guidance": "Todos los proveedores de subida fallaron. Inténtalo de nuevo o pega un enlace manualmente.",
@@ -364,5 +364,6 @@
"more_threads_last_week": "{{count}} hilos desde {{currentTimeFilterName}} — [<1>Mostrar más hilos de la semana pasada</1>]",
"more_threads_last_month": "{{count}} hilos desde {{currentTimeFilterName}} — [<1>Ver más hilos del mes pasado</1>]",
"more_threads_last_year": "{{count}} hilos desde {{currentTimeFilterName}} — [<1>Ver más hilos del año pasado</1>]",
"iframe_challenge_confirm": "{{board}} quiere abrir {{site}}.\n\nPara {{publicationType}}: {{excerpt}}"
"iframe_challenge_confirm": "{{board}} quiere abrir {{site}}.\n\nPara {{publicationType}}: {{excerpt}}",
"media_hosting_provider_unavailable": "Unavailable from this network. Try another network or a VPN."
}
+3 -2
View File
@@ -256,7 +256,7 @@
"upload_failed_all_providers": "همه ارائه‌دهندگان ناموفق بودند",
"upload_failed_preferred_guidance": "ارائه‌دهنده دیگری انتخاب کنید یا روی تصادفی تنظیم کنید.",
"media_hosting_random": "تصادفی",
"media_hosting_preferred": "ارائه‌دهنده ترجیحی",
"media_hosting_preferred": "ارائه‌دهنده را انتخاب کنید",
"media_hosting_preferred_provider_label": "ارائه‌دهنده را انتخاب کنید",
"media_hosting_preferred_failure_guidance": "اگر ارائه‌دهنده ترجیحی شما ناموفق باشد، برنامه ارائه‌دهندگان دیگر را امتحان می‌کند (حالت تصادفی) یا خطا نمایش می‌دهد (حالت ترجیحی).",
"media_hosting_all_providers_failed_guidance": "همه ارائه‌دهندگان آپلود ناموفق بودند. دوباره تلاش کنید یا لینک را دستی جایگذاری کنید.",
@@ -364,5 +364,6 @@
"more_threads_last_week": "{{count}} موضوع از {{currentTimeFilterName}} — [<1>نمایش موضوعات بیشتر از هفته گذشته</1>]",
"more_threads_last_month": "{{count}} پست از {{currentTimeFilterName}} — [<1>نمایش بیشتر پست‌های ماه گذشته</1>]",
"more_threads_last_year": "{{count}} پست از {{currentTimeFilterName}} — [<1>نمایش بیشتر پست‌های سال گذشته</1>]",
"iframe_challenge_confirm": "{{board}} می‌خواهد {{site}} را باز کند.\n\nبرای {{publicationType}}: {{excerpt}}"
"iframe_challenge_confirm": "{{board}} می‌خواهد {{site}} را باز کند.\n\nبرای {{publicationType}}: {{excerpt}}",
"media_hosting_provider_unavailable": "Unavailable from this network. Try another network or a VPN."
}
+3 -2
View File
@@ -256,7 +256,7 @@
"upload_failed_all_providers": "Kaikki palveluntarjoajat epäonnistuivat",
"upload_failed_preferred_guidance": "Valitse toinen palveluntarjoaja tai aseta satunnaiseksi.",
"media_hosting_random": "Satunnainen",
"media_hosting_preferred": "Suosikki palveluntarjoaja",
"media_hosting_preferred": "Valitse palveluntarjoaja",
"media_hosting_preferred_provider_label": "Valitse palveluntarjoaja",
"media_hosting_preferred_failure_guidance": "Jos suosikki palveluntarjoajasi epäonnistuu, sovellus kokeilee muita palveluntarjoajia (satunnainen tila) tai näyttää virheen (suosikki tila).",
"media_hosting_all_providers_failed_guidance": "Kaikki latauspalveluntarjoajat epäonnistuivat. Yritä uudelleen tai liitä linkki manuaalisesti.",
@@ -364,5 +364,6 @@
"more_threads_last_week": "{{count}} ketjua sitten {{currentTimeFilterName}} — [<1>Näytä lisää ketjuja viime viikolta</1>]",
"more_threads_last_month": "{{count}} ketjua sitten {{currentTimeFilterName}} — [<1>Näytä lisää viime kuun ketjuja</1>]",
"more_threads_last_year": "{{count}} ketjua sitten {{currentTimeFilterName}} — [<1>Näytä lisää viime vuoden ketjuja</1>]",
"iframe_challenge_confirm": "{{board}} haluaa avata sivuston {{site}}.\n\nKohteelle {{publicationType}}: {{excerpt}}"
"iframe_challenge_confirm": "{{board}} haluaa avata sivuston {{site}}.\n\nKohteelle {{publicationType}}: {{excerpt}}",
"media_hosting_provider_unavailable": "Unavailable from this network. Try another network or a VPN."
}
+3 -2
View File
@@ -256,7 +256,7 @@
"upload_failed_all_providers": "Lahat ng mga provider ay nabigo",
"upload_failed_preferred_guidance": "Pumili ng ibang provider o itakda sa random.",
"media_hosting_random": "Random",
"media_hosting_preferred": "Gustong provider",
"media_hosting_preferred": "Pumili ng provider",
"media_hosting_preferred_provider_label": "Pumili ng provider",
"media_hosting_preferred_failure_guidance": "Kung nabigo ang iyong gustong provider, susubukan ng app ang iba pang provider (Random mode) o magpapakita ng error (Preferred mode).",
"media_hosting_all_providers_failed_guidance": "Lahat ng mga upload provider ay nabigo. Subukan muli o i-paste ang link nang manu-mano.",
@@ -364,5 +364,6 @@
"more_threads_last_week": "{{count}} mga thread mula sa {{currentTimeFilterName}} — [<1>Ipakita ang higit pang mga thread mula sa nakaraang linggo</1>]",
"more_threads_last_month": "{{count}} mga thread mula {{currentTimeFilterName}} — [<1>Ipakita ang higit pang mga thread mula noong nakaraang buwan</1>]",
"more_threads_last_year": "{{count}} mga thread mula {{currentTimeFilterName}} — [<1>Ipakita ang higit pang mga thread mula noong nakaraang taon</1>]",
"iframe_challenge_confirm": "Nais buksan ng {{board}} ang {{site}}.\n\nPara sa {{publicationType}}: {{excerpt}}"
"iframe_challenge_confirm": "Nais buksan ng {{board}} ang {{site}}.\n\nPara sa {{publicationType}}: {{excerpt}}",
"media_hosting_provider_unavailable": "Unavailable from this network. Try another network or a VPN."
}
+3 -2
View File
@@ -256,7 +256,7 @@
"upload_failed_all_providers": "Tous les fournisseurs ont échoué",
"upload_failed_preferred_guidance": "Choisissez un autre fournisseur ou réglez sur aléatoire.",
"media_hosting_random": "Aléatoire",
"media_hosting_preferred": "Fournisseur préféré",
"media_hosting_preferred": "Choisir le fournisseur",
"media_hosting_preferred_provider_label": "Choisir le fournisseur",
"media_hosting_preferred_failure_guidance": "Si votre fournisseur préféré échoue, l'app essaiera d'autres fournisseurs (mode aléatoire) ou affichera une erreur (mode préféré).",
"media_hosting_all_providers_failed_guidance": "Tous les fournisseurs de téléchargement ont échoué. Réessayez ou collez un lien manuellement.",
@@ -364,5 +364,6 @@
"more_threads_last_week": "{{count}} fils depuis {{currentTimeFilterName}} — [<1>Afficher plus de fils de la semaine dernière</1>]",
"more_threads_last_month": "{{count}} fils depuis {{currentTimeFilterName}} — [<1>Afficher plus de fils du mois dernier</1>]",
"more_threads_last_year": "{{count}} fils depuis {{currentTimeFilterName}} — [<1>Afficher plus de fils de l'année dernière</1>]",
"iframe_challenge_confirm": "{{board}} veut ouvrir {{site}}.\n\nPour {{publicationType}} : {{excerpt}}"
"iframe_challenge_confirm": "{{board}} veut ouvrir {{site}}.\n\nPour {{publicationType}} : {{excerpt}}",
"media_hosting_provider_unavailable": "Unavailable from this network. Try another network or a VPN."
}
+3 -2
View File
@@ -256,7 +256,7 @@
"upload_failed_all_providers": "כל הספקים נכשלו",
"upload_failed_preferred_guidance": "בחר ספק אחר או הגדר לאקראי.",
"media_hosting_random": "אקראי",
"media_hosting_preferred": "ספק מועדף",
"media_hosting_preferred": "בחר ספק",
"media_hosting_preferred_provider_label": "בחר ספק",
"media_hosting_preferred_failure_guidance": "אם הספק המועדף שלך נכשל, האפליקציה תנסה ספקים אחרים (מצב אקראי) או תציג שגיאה (מצב מועדף).",
"media_hosting_all_providers_failed_guidance": "כל ספקי ההעלאה נכשלו. נסה שוב או הדבק קישור ידנית.",
@@ -364,5 +364,6 @@
"more_threads_last_week": "{{count}} אשכולות מאז {{currentTimeFilterName}} — [<1>הצג עוד אשכולות מהשבוע שעבר</1>]",
"more_threads_last_month": "{{count}} חוטים מאז {{currentTimeFilterName}} — [<1>הצג עוד חוטים מהחודש שעבר</1>]",
"more_threads_last_year": "{{count}} חוטים מאז {{currentTimeFilterName}} — [<1>הצג עוד חוטים מהשנה שעברה</1>]",
"iframe_challenge_confirm": "{{board}} רוצה לפתוח את {{site}}.\n\nעבור {{publicationType}}: {{excerpt}}"
"iframe_challenge_confirm": "{{board}} רוצה לפתוח את {{site}}.\n\nעבור {{publicationType}}: {{excerpt}}",
"media_hosting_provider_unavailable": "Unavailable from this network. Try another network or a VPN."
}
+3 -2
View File
@@ -256,7 +256,7 @@
"upload_failed_all_providers": "सभी प्रदाता विफल हो गए",
"upload_failed_preferred_guidance": "कोई अलग प्रदाता चुनें या यादृच्छिक पर सेट करें।",
"media_hosting_random": "यादृच्छिक",
"media_hosting_preferred": "पसंदीदा प्रदाता",
"media_hosting_preferred": "प्रदाता चुनें",
"media_hosting_preferred_provider_label": "प्रदाता चुनें",
"media_hosting_preferred_failure_guidance": "यदि आपका पसंदीदा प्रदाता विफल होता है, ऐप अन्य प्रदाताओं को आज़माएगा (यादृच्छिक मोड) या त्रुटि दिखाएगा (पसंदीदा मोड)।",
"media_hosting_all_providers_failed_guidance": "सभी अपलोड प्रदाता विफल हो गए। पुनः प्रयास करें या लिंक मैन्युअल रूप से पेस्ट करें।",
@@ -364,5 +364,6 @@
"more_threads_last_week": "{{count}} थ्रेड {{currentTimeFilterName}} से — [<1>पिछले सप्ताह के और थ्रेड दिखाएं</1>]",
"more_threads_last_month": "{{count}} थ्रेड्स {{currentTimeFilterName}} से — [<1>अगले महीने के अधिक थ्रेड्स दिखाएं</1>]",
"more_threads_last_year": "{{count}} थ्रेड्स {{currentTimeFilterName}} से — [<1>पिछले साल के अधिक थ्रेड्स दिखाएं</1>]",
"iframe_challenge_confirm": "{{board}} {{site}} खोलना चाहता है।\n\n{{publicationType}} के लिए: {{excerpt}}"
"iframe_challenge_confirm": "{{board}} {{site}} खोलना चाहता है।\n\n{{publicationType}} के लिए: {{excerpt}}",
"media_hosting_provider_unavailable": "Unavailable from this network. Try another network or a VPN."
}
+3 -2
View File
@@ -256,7 +256,7 @@
"upload_failed_all_providers": "Minden szolgáltató sikertelen volt",
"upload_failed_preferred_guidance": "Válasszon másik szolgáltatót vagy állítsa véletlenszerűre.",
"media_hosting_random": "Véletlenszerű",
"media_hosting_preferred": "Előnyben részesített szolgáltató",
"media_hosting_preferred": "Szolgáltató kiválasztása",
"media_hosting_preferred_provider_label": "Szolgáltató kiválasztása",
"media_hosting_preferred_failure_guidance": "Ha az előnyben részesített szolgáltató sikertelen, az alkalmazás más szolgáltatókat próbál (véletlenszerű mód) vagy hibát jelenít meg (előnyben részesített mód).",
"media_hosting_all_providers_failed_guidance": "Minden feltöltési szolgáltató sikertelen volt. Próbálja újra vagy illessze be a linket manuálisan.",
@@ -364,5 +364,6 @@
"more_threads_last_week": "{{count}} szál a {{currentTimeFilterName}} óta — [<1>Mutass több szálat a múlt héten</1>]",
"more_threads_last_month": "{{count}} szál {{currentTimeFilterName}} óta — [<1>Több szál megjelenítése a múlt hónapból</1>]",
"more_threads_last_year": "{{count}} szál {{currentTimeFilterName}} óta — [<1>Több szál megjelenítése a múlt évből</1>]",
"iframe_challenge_confirm": "{{board}} meg szeretné nyitni a következőt: {{site}}.\n\n{{publicationType}} esetén: {{excerpt}}"
"iframe_challenge_confirm": "{{board}} meg szeretné nyitni a következőt: {{site}}.\n\n{{publicationType}} esetén: {{excerpt}}",
"media_hosting_provider_unavailable": "Unavailable from this network. Try another network or a VPN."
}
+3 -2
View File
@@ -256,7 +256,7 @@
"upload_failed_all_providers": "Semua penyedia gagal",
"upload_failed_preferred_guidance": "Pilih penyedia lain atau atur ke acak.",
"media_hosting_random": "Acak",
"media_hosting_preferred": "Penyedia pilihan",
"media_hosting_preferred": "Pilih penyedia",
"media_hosting_preferred_provider_label": "Pilih penyedia",
"media_hosting_preferred_failure_guidance": "Jika penyedia pilihan Anda gagal, aplikasi akan mencoba penyedia lain (mode acak) atau menampilkan error (mode preferensi).",
"media_hosting_all_providers_failed_guidance": "Semua penyedia unggahan gagal. Coba lagi atau tempel link secara manual.",
@@ -364,5 +364,6 @@
"more_threads_last_week": "{{count}} thread sejak {{currentTimeFilterName}} — [<1>Show lebih banyak thread dari minggu lalu</1>]",
"more_threads_last_month": "{{count}} thread sejak {{currentTimeFilterName}} — [<1>Tampilkan lebih banyak thread dari bulan lalu</1>]",
"more_threads_last_year": "{{count}} thread sejak {{currentTimeFilterName}} — [<1>Tampilkan lebih banyak thread dari tahun lalu</1>]",
"iframe_challenge_confirm": "{{board}} ingin membuka {{site}}.\n\nUntuk {{publicationType}}: {{excerpt}}"
"iframe_challenge_confirm": "{{board}} ingin membuka {{site}}.\n\nUntuk {{publicationType}}: {{excerpt}}",
"media_hosting_provider_unavailable": "Unavailable from this network. Try another network or a VPN."
}
+3 -2
View File
@@ -256,7 +256,7 @@
"upload_failed_all_providers": "Tutti i provider sono falliti",
"upload_failed_preferred_guidance": "Scegli un provider diverso o imposta su casuale.",
"media_hosting_random": "Casuale",
"media_hosting_preferred": "Provider preferito",
"media_hosting_preferred": "Scegli provider",
"media_hosting_preferred_provider_label": "Scegli provider",
"media_hosting_preferred_failure_guidance": "Se il provider preferito fallisce, l'app proverà altri provider (modalità casuale) o mostrerà un errore (modalità preferita).",
"media_hosting_all_providers_failed_guidance": "Tutti i provider di caricamento sono falliti. Riprova o incolla un link manualmente.",
@@ -364,5 +364,6 @@
"more_threads_last_week": "{{count}} thread da {{currentTimeFilterName}} — [<1>Mostra più thread dalla scorsa settimana</1>]",
"more_threads_last_month": "{{count}} thread da {{currentTimeFilterName}} — [<1>Mostra altri thread del mese scorso</1>]",
"more_threads_last_year": "{{count}} thread da {{currentTimeFilterName}} — [<1>Mostra altri thread dell'anno scorso</1>]",
"iframe_challenge_confirm": "{{board}} vuole aprire {{site}}.\n\nPer {{publicationType}}: {{excerpt}}"
"iframe_challenge_confirm": "{{board}} vuole aprire {{site}}.\n\nPer {{publicationType}}: {{excerpt}}",
"media_hosting_provider_unavailable": "Unavailable from this network. Try another network or a VPN."
}
+3 -2
View File
@@ -256,7 +256,7 @@
"upload_failed_all_providers": "すべてのプロバイダーが失敗しました",
"upload_failed_preferred_guidance": "別のプロバイダーを選択するか、ランダムに設定してください。",
"media_hosting_random": "ランダム",
"media_hosting_preferred": "優先プロバイダー",
"media_hosting_preferred": "プロバイダーを選択",
"media_hosting_preferred_provider_label": "プロバイダーを選択",
"media_hosting_preferred_failure_guidance": "優先プロバイダーが失敗した場合、アプリは他のプロバイダーを試行します(ランダムモード)またはエラーを表示します(優先モード)。",
"media_hosting_all_providers_failed_guidance": "すべてのアップロードプロバイダーが失敗しました。もう一度お試しいただくか、リンクを手動で貼り付けてください。",
@@ -364,5 +364,6 @@
"more_threads_last_week": "{{count}} スレッド {{currentTimeFilterName}} から — [<1>先週のスレッドをさらに表示</1>]",
"more_threads_last_month": "{{count}} スレッド {{currentTimeFilterName}} から — [<1>先月のスレッドをさらに表示</1>]",
"more_threads_last_year": "{{count}} スレッド {{currentTimeFilterName}} から — [<1>去年のスレッドをさらに表示</1>]",
"iframe_challenge_confirm": "{{board}}が{{site}}を開こうとしています。\n\n{{publicationType}}の場合: {{excerpt}}"
"iframe_challenge_confirm": "{{board}}が{{site}}を開こうとしています。\n\n{{publicationType}}の場合: {{excerpt}}",
"media_hosting_provider_unavailable": "Unavailable from this network. Try another network or a VPN."
}
+3 -2
View File
@@ -256,7 +256,7 @@
"upload_failed_all_providers": "모든 제공업체가 실패했습니다",
"upload_failed_preferred_guidance": "다른 제공업체를 선택하거나 무작위로 설정하세요.",
"media_hosting_random": "무작위",
"media_hosting_preferred": "선호하는 제공업체",
"media_hosting_preferred": "제공업체 선택",
"media_hosting_preferred_provider_label": "제공업체 선택",
"media_hosting_preferred_failure_guidance": "선호하는 제공업체가 실패하면 앱이 다른 제공업체를 시도합니다(무작위 모드) 또는 오류를 표시합니다(선호 모드).",
"media_hosting_all_providers_failed_guidance": "모든 업로드 제공업체가 실패했습니다. 다시 시도하거나 링크를 수동으로 붙여넣으세요.",
@@ -364,5 +364,6 @@
"more_threads_last_week": "{{count}} 스레드 {{currentTimeFilterName}} 이후 — [<1>지난 주의 더 많은 스레드 보기</1>]",
"more_threads_last_month": "{{count}} 스레드 {{currentTimeFilterName}} 이후 — [<1>지난 달의 스레드 더 보기</1>]",
"more_threads_last_year": "{{count}} 스레드 {{currentTimeFilterName}} 이후 — [<1>작년의 스레드 더 보기</1>]",
"iframe_challenge_confirm": "{{board}}에서 {{site}} 열기를 요청했습니다.\n\n{{publicationType}}의 경우: {{excerpt}}"
"iframe_challenge_confirm": "{{board}}에서 {{site}} 열기를 요청했습니다.\n\n{{publicationType}}의 경우: {{excerpt}}",
"media_hosting_provider_unavailable": "Unavailable from this network. Try another network or a VPN."
}
+3 -2
View File
@@ -256,7 +256,7 @@
"upload_failed_all_providers": "सर्व प्रदाते अयशस्वी झाले",
"upload_failed_preferred_guidance": "वेगळा प्रदाता निवडा किंवा यादृच्छिक वर सेट करा.",
"media_hosting_random": "यादृच्छिक",
"media_hosting_preferred": "पसंतीचा प्रदाता",
"media_hosting_preferred": "प्रदाता निवडा",
"media_hosting_preferred_provider_label": "प्रदाता निवडा",
"media_hosting_preferred_failure_guidance": "जर तुमचा पसंतीचा प्रदाता अयशस्वी झाला, तर अॅप इतर प्रदात्यांना वापरेल (यादृच्छिक मोड) किंवा त्रुटी दर्शवेल (पसंतीचा मोड).",
"media_hosting_all_providers_failed_guidance": "सर्व अपलोड प्रदाते अयशस्वी झाले. पुन्हा प्रयत्न करा किंवा लिंक स्वहस्ते पेस्ट करा.",
@@ -364,5 +364,6 @@
"more_threads_last_week": "{{count}} थ्रेड {{currentTimeFilterName}} पासून — [<1>गेल्या आठवड्यातील अधिक थ्रेड्स पहा</1>]",
"more_threads_last_month": "{{count}} ध糀ं {{currentTimeFilterName}} पासून — [<1>गेल्या महिन्यातील आणखी थ्रेड्स दाखवा</1>]",
"more_threads_last_year": "{{count}} ध糀ं {{currentTimeFilterName}} पासून — [<1>गेल्या वर्षातील आणखी थ्रेड्स दाखवा</1>]",
"iframe_challenge_confirm": "{{board}} ला {{site}} उघडायचे आहे.\n\n{{publicationType}} साठी: {{excerpt}}"
"iframe_challenge_confirm": "{{board}} ला {{site}} उघडायचे आहे.\n\n{{publicationType}} साठी: {{excerpt}}",
"media_hosting_provider_unavailable": "Unavailable from this network. Try another network or a VPN."
}
+3 -2
View File
@@ -256,7 +256,7 @@
"upload_failed_all_providers": "Alle providers zijn mislukt",
"upload_failed_preferred_guidance": "Kies een andere provider of zet op willekeurig.",
"media_hosting_random": "Willekeurig",
"media_hosting_preferred": "Voorkeursprovider",
"media_hosting_preferred": "Provider kiezen",
"media_hosting_preferred_provider_label": "Provider kiezen",
"media_hosting_preferred_failure_guidance": "Als uw voorkeursprovider faalt, probeert de app andere providers (willekeurige modus) of toont een fout (geprefereerde modus).",
"media_hosting_all_providers_failed_guidance": "Alle upload-providers zijn mislukt. Probeer opnieuw of plak een link handmatig.",
@@ -364,5 +364,6 @@
"more_threads_last_week": "{{count}} threads sinds {{currentTimeFilterName}} — [<1>Toon meer threads van vorige week</1>]",
"more_threads_last_month": "{{count}} threads sinds {{currentTimeFilterName}} — [<1>Meer threads van afgelopen maand tonen</1>]",
"more_threads_last_year": "{{count}} threads sinds {{currentTimeFilterName}} — [<1>Meer threads van afgelopen jaar tonen</1>]",
"iframe_challenge_confirm": "{{board}} wil {{site}} openen.\n\nVoor {{publicationType}}: {{excerpt}}"
"iframe_challenge_confirm": "{{board}} wil {{site}} openen.\n\nVoor {{publicationType}}: {{excerpt}}",
"media_hosting_provider_unavailable": "Unavailable from this network. Try another network or a VPN."
}
+3 -2
View File
@@ -256,7 +256,7 @@
"upload_failed_all_providers": "Alle leverandører mislyktes",
"upload_failed_preferred_guidance": "Velg en annen leverandør eller sett til tilfeldig.",
"media_hosting_random": "Tilfeldig",
"media_hosting_preferred": "Foretrukket leverandør",
"media_hosting_preferred": "Velg leverandør",
"media_hosting_preferred_provider_label": "Velg leverandør",
"media_hosting_preferred_failure_guidance": "Hvis din foretrukne leverandør feiler, vil appen prøve andre leverandører (tilfeldig modus) eller vise en feil (foretrukket modus).",
"media_hosting_all_providers_failed_guidance": "Alle opplastingsleverandører mislyktes. Prøv igjen eller lim inn en lenke manuelt.",
@@ -364,5 +364,6 @@
"more_threads_last_week": "{{count}} tråder siden {{currentTimeFilterName}} — [<1>Vis flere tråder fra forrige uke</1>]",
"more_threads_last_month": "{{count}} tråder siden {{currentTimeFilterName}} — [<1>Vis flere tråder fra forrige måned</1>]",
"more_threads_last_year": "{{count}} tråder siden {{currentTimeFilterName}} — [<1>Vis flere tråder fra forrige år</1>]",
"iframe_challenge_confirm": "{{board}} vil åpne {{site}}.\n\nFor {{publicationType}}: {{excerpt}}"
"iframe_challenge_confirm": "{{board}} vil åpne {{site}}.\n\nFor {{publicationType}}: {{excerpt}}",
"media_hosting_provider_unavailable": "Unavailable from this network. Try another network or a VPN."
}
+3 -2
View File
@@ -256,7 +256,7 @@
"upload_failed_all_providers": "Wszyscy dostawcy zawiedli",
"upload_failed_preferred_guidance": "Wybierz innego dostawcę lub ustaw na losowy.",
"media_hosting_random": "Losowy",
"media_hosting_preferred": "Preferowany dostawca",
"media_hosting_preferred": "Wybierz dostawcę",
"media_hosting_preferred_provider_label": "Wybierz dostawcę",
"media_hosting_preferred_failure_guidance": "Jeśli preferowany dostawca zawiedzie, aplikacja spróbuje innych dostawców (tryb losowy) lub wyświetli błąd (tryb preferowany).",
"media_hosting_all_providers_failed_guidance": "Wszyscy dostawcy przesyłania zawiedli. Spróbuj ponownie lub wklej link ręcznie.",
@@ -364,5 +364,6 @@
"more_threads_last_week": "{{count}} wątki od {{currentTimeFilterName}} — [<1>Pokaż więcej wątków z zeszłego tygodnia</1>]",
"more_threads_last_month": "{{count}} wątki od {{currentTimeFilterName}} — [<1>Pokaż więcej wątków z zeszłego miesiąca</1>]",
"more_threads_last_year": "{{count}} wątki od {{currentTimeFilterName}} — [<1>Pokaż więcej wątków z zeszłego roku</1>]",
"iframe_challenge_confirm": "{{board}} chce otworzyć {{site}}.\n\nDla {{publicationType}}: {{excerpt}}"
"iframe_challenge_confirm": "{{board}} chce otworzyć {{site}}.\n\nDla {{publicationType}}: {{excerpt}}",
"media_hosting_provider_unavailable": "Unavailable from this network. Try another network or a VPN."
}
+3 -2
View File
@@ -256,7 +256,7 @@
"upload_failed_all_providers": "Todos os provedores falharam",
"upload_failed_preferred_guidance": "Escolha um provedor diferente ou defina como aleatório.",
"media_hosting_random": "Aleatório",
"media_hosting_preferred": "Provedor preferido",
"media_hosting_preferred": "Escolher provedor",
"media_hosting_preferred_provider_label": "Escolher provedor",
"media_hosting_preferred_failure_guidance": "Se o provedor preferido falhar, o app tentará outros provedores (modo aleatório) ou mostrará um erro (modo preferido).",
"media_hosting_all_providers_failed_guidance": "Todos os provedores de envio falharam. Tente novamente ou cole um link manualmente.",
@@ -364,5 +364,6 @@
"more_threads_last_week": "{{count}} tópicos desde {{currentTimeFilterName}} — [<1>Mostrar mais tópicos da semana passada</1>]",
"more_threads_last_month": "{{count}} tópicos desde {{currentTimeFilterName}} — [<1>Mostrar mais tópicos do mês passado</1>]",
"more_threads_last_year": "{{count}} tópicos desde {{currentTimeFilterName}} — [<1>Mostrar mais tópicos do ano passado</1>]",
"iframe_challenge_confirm": "{{board}} quer abrir {{site}}.\n\nPara {{publicationType}}: {{excerpt}}"
"iframe_challenge_confirm": "{{board}} quer abrir {{site}}.\n\nPara {{publicationType}}: {{excerpt}}",
"media_hosting_provider_unavailable": "Unavailable from this network. Try another network or a VPN."
}
+3 -2
View File
@@ -256,7 +256,7 @@
"upload_failed_all_providers": "Toți furnizorii au eșuat",
"upload_failed_preferred_guidance": "Alegeți un furnizor diferit sau setați la aleatoriu.",
"media_hosting_random": "Aleatoriu",
"media_hosting_preferred": "Furnizor preferat",
"media_hosting_preferred": "Alegeți furnizorul",
"media_hosting_preferred_provider_label": "Alegeți furnizorul",
"media_hosting_preferred_failure_guidance": "Dacă furnizorul preferat eșuează, aplicația va încerca alți furnizori (mod aleatoriu) sau va afișa o eroare (mod preferat).",
"media_hosting_all_providers_failed_guidance": "Toți furnizorii de încărcare au eșuat. Încercați din nou sau lipiți un link manual.",
@@ -364,5 +364,6 @@
"more_threads_last_week": "{{count}} fire de discuție de la {{currentTimeFilterName}} — [<1>Arată mai multe fire din săptămâna trecută</1>]",
"more_threads_last_month": "{{count}} subiecte din {{currentTimeFilterName}} — [<1>Afișați mai multe subiecte de luna trecută</1>]",
"more_threads_last_year": "{{count}} subiecte din {{currentTimeFilterName}} — [<1>Afișați mai multe subiecte de anul trecut</1>]",
"iframe_challenge_confirm": "{{board}} vrea să deschidă {{site}}.\n\nPentru {{publicationType}}: {{excerpt}}"
"iframe_challenge_confirm": "{{board}} vrea să deschidă {{site}}.\n\nPentru {{publicationType}}: {{excerpt}}",
"media_hosting_provider_unavailable": "Unavailable from this network. Try another network or a VPN."
}
+3 -2
View File
@@ -256,7 +256,7 @@
"upload_failed_all_providers": "Все провайдеры не справились",
"upload_failed_preferred_guidance": "Выберите другого провайдера или установите случайный.",
"media_hosting_random": "Случайный",
"media_hosting_preferred": "Предпочтительный провайдер",
"media_hosting_preferred": "Выбрать провайдера",
"media_hosting_preferred_provider_label": "Выбрать провайдера",
"media_hosting_preferred_failure_guidance": "Если предпочтительный провайдер не сработает, приложение попробует других провайдеров (случайный режим) или покажет ошибку (режим предпочтительный).",
"media_hosting_all_providers_failed_guidance": "Все провайдеры загрузки не справились. Попробуйте снова или вставьте ссылку вручную.",
@@ -364,5 +364,6 @@
"more_threads_last_week": "{{count}} тредов с {{currentTimeFilterName}} — [<1>Показать больше тредов за прошлую неделю</1>]",
"more_threads_last_month": "{{count}} темы с {{currentTimeFilterName}} — [<1>Показать больше тем с прошлого месяца</1>]",
"more_threads_last_year": "{{count}} темы с {{currentTimeFilterName}} — [<1>Показать больше тем с прошлого года</1>]",
"iframe_challenge_confirm": "{{board}} хочет открыть {{site}}.\n\nДля {{publicationType}}: {{excerpt}}"
"iframe_challenge_confirm": "{{board}} хочет открыть {{site}}.\n\nДля {{publicationType}}: {{excerpt}}",
"media_hosting_provider_unavailable": "Unavailable from this network. Try another network or a VPN."
}
+3 -2
View File
@@ -256,7 +256,7 @@
"upload_failed_all_providers": "Të gjithë ofruesit dështuan",
"upload_failed_preferred_guidance": "Zgjidhni një ofrues tjetër ose vendosni në rastësishëm.",
"media_hosting_random": "Rastësishëm",
"media_hosting_preferred": "Ofrues i preferuar",
"media_hosting_preferred": "Zgjidhni ofruesin",
"media_hosting_preferred_provider_label": "Zgjidhni ofruesin",
"media_hosting_preferred_failure_guidance": "Nëse ofruesi juaj i preferuar dështon, aplikacioni do të provojë ofrues të tjerë (modalitet i rastësishëm) ose do të shfaqë një gabim (modalitet i preferuar).",
"media_hosting_all_providers_failed_guidance": "Të gjithë ofruesit e ngarkimit dështuan. Provoni përsëri ose ngjisni një lidhje manualisht.",
@@ -364,5 +364,6 @@
"more_threads_last_week": "{{count}} tema nga {{currentTimeFilterName}} — [<1>Shfaq më shumë tema nga java e kaluar</1>]",
"more_threads_last_month": "{{count}} tema nga {{currentTimeFilterName}} — [<1>Shfaq më shumë tema nga muaji i kaluar</1>]",
"more_threads_last_year": "{{count}} tema nga {{currentTimeFilterName}} — [<1>Shfaq më shumë tema nga viti i kaluar</1>]",
"iframe_challenge_confirm": "{{board}} dëshiron të hapë {{site}}.\n\nPër {{publicationType}}: {{excerpt}}"
"iframe_challenge_confirm": "{{board}} dëshiron të hapë {{site}}.\n\nPër {{publicationType}}: {{excerpt}}",
"media_hosting_provider_unavailable": "Unavailable from this network. Try another network or a VPN."
}
+3 -2
View File
@@ -256,7 +256,7 @@
"upload_failed_all_providers": "Alla leverantörer misslyckades",
"upload_failed_preferred_guidance": "Välj en annan leverantör eller sätt till slumpmässig.",
"media_hosting_random": "Slumpmässig",
"media_hosting_preferred": "Föredragen leverantör",
"media_hosting_preferred": "Välj leverantör",
"media_hosting_preferred_provider_label": "Välj leverantör",
"media_hosting_preferred_failure_guidance": "Om din föredragna leverantör misslyckas kommer appen att prova andra leverantörer (slumpmässigt läge) eller visa ett fel (föredraget läge).",
"media_hosting_all_providers_failed_guidance": "Alla uppladdningsleverantörer misslyckades. Försök igen eller klistra in en länk manuellt.",
@@ -364,5 +364,6 @@
"more_threads_last_week": "{{count}} trådar sedan {{currentTimeFilterName}} — [<1>Visa fler trådar från förra veckan</1>]",
"more_threads_last_month": "{{count}} trådar sedan {{currentTimeFilterName}} — [<1>Visa fler trådar från förra månaden</1>]",
"more_threads_last_year": "{{count}} trådar sedan {{currentTimeFilterName}} — [<1>Visa fler trådar från förra året</1>]",
"iframe_challenge_confirm": "{{board}} vill öppna {{site}}.\n\nFör {{publicationType}}: {{excerpt}}"
"iframe_challenge_confirm": "{{board}} vill öppna {{site}}.\n\nFör {{publicationType}}: {{excerpt}}",
"media_hosting_provider_unavailable": "Unavailable from this network. Try another network or a VPN."
}
+3 -2
View File
@@ -256,7 +256,7 @@
"upload_failed_all_providers": "అన్ని ప్రొవైడర్లు విఫలమయ్యాయి",
"upload_failed_preferred_guidance": "వేరే ప్రొవైడర్ ఎంచుకోండి లేదా యాదృచ్ఛికంగా సెట్ చేయండి.",
"media_hosting_random": "యాదృచ్ఛికం",
"media_hosting_preferred": "ఇష్టప్రదమైన ప్రొవైడర్",
"media_hosting_preferred": "ప్రొవైడర్ ఎంచుకోండి",
"media_hosting_preferred_provider_label": "ప్రొవైడర్ ఎంచుకోండి",
"media_hosting_preferred_failure_guidance": "మీ ఇష్టప్రదమైన ప్రొవైడర్ విఫలమైతే, యాప్ ఇతర ప్రొవైడర్లను ప్రయత్నిస్తుంది (యాదృచ్ఛిక మోడ్) లేదా లోపం చూపిస్తుంది (ఇష్టప్రదమైన మోడ్).",
"media_hosting_all_providers_failed_guidance": "అన్ని అప్‌లోడ్ ప్రొవైడర్లు విఫలమయ్యాయి. మళ్ళీ ప్రయత్నించండి లేదా లింక్‌ను మాన్యువల్‌గా అతికించండి.",
@@ -364,5 +364,6 @@
"more_threads_last_week": "{{count}} థ్రెడ్‌లు {{currentTimeFilterName}} నుండి — [<1>గత వారానికి మరిన్ని థ్రెడ్‌లు చూపించు</1>]",
"more_threads_last_month": "{{count}} థ్రెడ్‌లు {{currentTimeFilterName}} నుండి — [<1>గత నెల నుండి మరిన్ని థ్రెడ్‌లను చూపించండి</1>]",
"more_threads_last_year": "{{count}} థ్రెడ్‌లు {{currentTimeFilterName}} నుండి — [<1>గత సంవత్సరం నుండి మరిన్ని థ్రెడ్‌లను చూపించండి</1>]",
"iframe_challenge_confirm": "{{board}} {{site}} ను తెరవాలని కోరుతోంది.\n\n{{publicationType}} కోసం: {{excerpt}}"
"iframe_challenge_confirm": "{{board}} {{site}} ను తెరవాలని కోరుతోంది.\n\n{{publicationType}} కోసం: {{excerpt}}",
"media_hosting_provider_unavailable": "Unavailable from this network. Try another network or a VPN."
}
+3 -2
View File
@@ -256,7 +256,7 @@
"upload_failed_all_providers": "ผู้ให้บริการทั้งหมดล้มเหลว",
"upload_failed_preferred_guidance": "เลือกผู้ให้บริการอื่นหรือตั้งเป็นแบบสุ่ม",
"media_hosting_random": "สุ่ม",
"media_hosting_preferred": "ผู้ให้บริการที่ต้องการ",
"media_hosting_preferred": "เลือกผู้ให้บริการ",
"media_hosting_preferred_provider_label": "เลือกผู้ให้บริการ",
"media_hosting_preferred_failure_guidance": "หากผู้ให้บริการที่คุณต้องการล้มเหลว แอปจะลองผู้ให้บริการอื่น (โหมดสุ่ม) หรือแสดงข้อผิดพลาด (โหมดที่ต้องการ)",
"media_hosting_all_providers_failed_guidance": "ผู้ให้บริการอัปโหลดทั้งหมดล้มเหลว ลองอีกครั้งหรือวางลิงก์ด้วยตนเอง",
@@ -364,5 +364,6 @@
"more_threads_last_week": "{{count}} กระทู้ตั้งแต่ {{currentTimeFilterName}} — [<1>แสดงกระทู้เพิ่มเติมจากสัปดาห์ที่แล้ว</1>]",
"more_threads_last_month": "{{count}} กระทู้ตั้งแต่ {{currentTimeFilterName}} — [<1>แสดงกระทู้จากเดือนที่แล้วเพิ่มเติม</1>]",
"more_threads_last_year": "{{count}} กระทู้ตั้งแต่ {{currentTimeFilterName}} — [<1>แสดงกระทู้จากปีที่แล้วเพิ่มเติม</1>]",
"iframe_challenge_confirm": "{{board}} ต้องการเปิด {{site}}\n\nสำหรับ {{publicationType}}: {{excerpt}}"
"iframe_challenge_confirm": "{{board}} ต้องการเปิด {{site}}\n\nสำหรับ {{publicationType}}: {{excerpt}}",
"media_hosting_provider_unavailable": "Unavailable from this network. Try another network or a VPN."
}
+3 -2
View File
@@ -256,7 +256,7 @@
"upload_failed_all_providers": "Tüm sağlayıcılar başarısız oldu",
"upload_failed_preferred_guidance": "Farklı bir sağlayıcı seçin veya rastgeleye ayarlayın.",
"media_hosting_random": "Rastgele",
"media_hosting_preferred": "Tercih edilen sağlayıcı",
"media_hosting_preferred": "Sağlayıcı seç",
"media_hosting_preferred_provider_label": "Sağlayıcı seç",
"media_hosting_preferred_failure_guidance": "Tercih edilen sağlayıcı başarısız olursa, uygulama diğer sağlayıcıları dener (Rastgele mod) veya hata gösterir (Tercih modu).",
"media_hosting_all_providers_failed_guidance": "Tüm yükleme sağlayıcıları başarısız oldu. Tekrar deneyin veya bir bağlantıyı manuel olarak yapıştırın.",
@@ -364,5 +364,6 @@
"more_threads_last_week": "{{count}} başlık {{currentTimeFilterName}}den itibaren — [<1>Geçen haftadan daha fazla başlık göster</1>]",
"more_threads_last_month": "{{count}} konu {{currentTimeFilterName}} itibariyle — [<1>Geçen aydan daha fazla konu göster</1>]",
"more_threads_last_year": "{{count}} konu {{currentTimeFilterName}} itibariyle — [<1>Geçen yıldan daha fazla konu göster</1>]",
"iframe_challenge_confirm": "{{board}}, {{site}} sitesini açmak istiyor.\n\n{{publicationType}} için: {{excerpt}}"
"iframe_challenge_confirm": "{{board}}, {{site}} sitesini açmak istiyor.\n\n{{publicationType}} için: {{excerpt}}",
"media_hosting_provider_unavailable": "Unavailable from this network. Try another network or a VPN."
}
+3 -2
View File
@@ -256,7 +256,7 @@
"upload_failed_all_providers": "Усі провайдери не справились",
"upload_failed_preferred_guidance": "Оберіть іншого провайдера або встановіть випадковий.",
"media_hosting_random": "Випадковий",
"media_hosting_preferred": "Уподобаний провайдер",
"media_hosting_preferred": "Оберіть провайдера",
"media_hosting_preferred_provider_label": "Оберіть провайдера",
"media_hosting_preferred_failure_guidance": "Якщо ваш уподобаний провайдер не спрацює, застосунок спробує інших провайдерів (випадковий режим) або покаже помилку (режим уподобання).",
"media_hosting_all_providers_failed_guidance": "Усі провайдери завантаження не справились. Спробуйте знову або вставте посилання вручну.",
@@ -364,5 +364,6 @@
"more_threads_last_week": "{{count}} тем з {{currentTimeFilterName}} — [<1>Показати більше тем з минулого тижня</1>]",
"more_threads_last_month": "{{count}} тем з {{currentTimeFilterName}} — [<1>Показати більше тем з минулого місяця</1>]",
"more_threads_last_year": "{{count}} тем з {{currentTimeFilterName}} — [<1>Показати більше тем з минулого року</1>]",
"iframe_challenge_confirm": "{{board}} хоче відкрити {{site}}.\n\nДля {{publicationType}}: {{excerpt}}"
"iframe_challenge_confirm": "{{board}} хоче відкрити {{site}}.\n\nДля {{publicationType}}: {{excerpt}}",
"media_hosting_provider_unavailable": "Unavailable from this network. Try another network or a VPN."
}
+3 -2
View File
@@ -256,7 +256,7 @@
"upload_failed_all_providers": "تمام فراہم کنندہ ناکام ہو گئے",
"upload_failed_preferred_guidance": "مختلف فراہم کنندہ منتخب کریں یا بے ترتیب پر سیٹ کریں۔",
"media_hosting_random": "بے ترتیب",
"media_hosting_preferred": "پسندیدہ فراہم کنندہ",
"media_hosting_preferred": "فراہم کنندہ منتخب کریں",
"media_hosting_preferred_provider_label": "فراہم کنندہ منتخب کریں",
"media_hosting_preferred_failure_guidance": "اگر آپ کا پسندیدہ فراہم کنندہ ناکام ہو جاتا ہے، ایپ دیگر فراہم کنندوں کو آزماے گی (بے ترتیب موڈ) یا خرابی دکھائے گی (پسندیدہ موڈ)۔",
"media_hosting_all_providers_failed_guidance": "تمام اپ لوڈ فراہم کنندہ ناکام ہو گئے۔ دوبارہ کوشش کریں یا لنک دستی طور پر پیسٹ کریں۔",
@@ -364,5 +364,6 @@
"more_threads_last_week": "{{count}} تھریڈ {{currentTimeFilterName}} سے — [<1>گزشتہ ہفتے کے مزید تھریڈ دیکھیں</1>]",
"more_threads_last_month": "{{count}} تھریڈز {{currentTimeFilterName}} سے — [<1>پچھلے ماہ کے مزید تھریڈز دکھائیں</1>]",
"more_threads_last_year": "{{count}} تھریڈز {{currentTimeFilterName}} سے — [<1>گزشتہ سال کے مزید تھریڈز دکھائیں</1>]",
"iframe_challenge_confirm": "{{board}}، {{site}} کھولنا چاہتا ہے۔\n\n{{publicationType}} کے لیے: {{excerpt}}"
"iframe_challenge_confirm": "{{board}}، {{site}} کھولنا چاہتا ہے۔\n\n{{publicationType}} کے لیے: {{excerpt}}",
"media_hosting_provider_unavailable": "Unavailable from this network. Try another network or a VPN."
}
+3 -2
View File
@@ -256,7 +256,7 @@
"upload_failed_all_providers": "Tất cả nhà cung cấp đều thất bại",
"upload_failed_preferred_guidance": "Chọn nhà cung cấp khác hoặc đặt thành ngẫu nhiên.",
"media_hosting_random": "Ngẫu nhiên",
"media_hosting_preferred": "Nhà cung cấp ưu tiên",
"media_hosting_preferred": "Chọn nhà cung cấp",
"media_hosting_preferred_provider_label": "Chọn nhà cung cấp",
"media_hosting_preferred_failure_guidance": "Nếu nhà cung cấp ưu tiên của bạn thất bại, ứng dụng sẽ thử các nhà cung cấp khác (chế độ ngẫu nhiên) hoặc hiển thị lỗi (chế độ ưu tiên).",
"media_hosting_all_providers_failed_guidance": "Tất cả nhà cung cấp tải lên đều thất bại. Thử lại hoặc dán liên kết thủ công.",
@@ -364,5 +364,6 @@
"more_threads_last_week": "{{count}} chủ đề từ {{currentTimeFilterName}} — [<1>Xem thêm chủ đề từ tuần trước</1>]",
"more_threads_last_month": "{{count}} chủ đề từ {{currentTimeFilterName}} — [<1>Hiển thị thêm các chủ đề từ tháng trước</1>]",
"more_threads_last_year": "{{count}} chủ đề từ {{currentTimeFilterName}} — [<1>Hiển thị thêm các chủ đề từ năm ngoái</1>]",
"iframe_challenge_confirm": "{{board}} muốn mở {{site}}.\n\nDành cho {{publicationType}}: {{excerpt}}"
"iframe_challenge_confirm": "{{board}} muốn mở {{site}}.\n\nDành cho {{publicationType}}: {{excerpt}}",
"media_hosting_provider_unavailable": "Unavailable from this network. Try another network or a VPN."
}
+3 -2
View File
@@ -256,7 +256,7 @@
"upload_failed_all_providers": "所有提供商均失败",
"upload_failed_preferred_guidance": "选择其他提供商或设置为随机。",
"media_hosting_random": "随机",
"media_hosting_preferred": "选提供商",
"media_hosting_preferred": "选提供商",
"media_hosting_preferred_provider_label": "选择提供商",
"media_hosting_preferred_failure_guidance": "如果首选提供商失败,应用将尝试其他提供商(随机模式)或显示错误(首选模式)。",
"media_hosting_all_providers_failed_guidance": "所有上传提供商均失败。请重试或手动粘贴链接。",
@@ -364,5 +364,6 @@
"more_threads_last_week": "{{count}} 主题自 {{currentTimeFilterName}} — [<1>显示更多来自上周的主题</1>]",
"more_threads_last_month": "{{count}} 主题自 {{currentTimeFilterName}} — [<1>显示更多上个月的主题</1>]",
"more_threads_last_year": "{{count}} 主题自 {{currentTimeFilterName}} — [<1>显示更多去年的主题</1>]",
"iframe_challenge_confirm": "{{board}} 想要打开 {{site}}。\n\n对于 {{publicationType}}{{excerpt}}"
"iframe_challenge_confirm": "{{board}} 想要打开 {{site}}。\n\n对于 {{publicationType}}{{excerpt}}",
"media_hosting_provider_unavailable": "Unavailable from this network. Try another network or a VPN."
}
@@ -17,11 +17,18 @@ vi.mock('@capacitor/core', () => ({
Capacitor: { getPlatform: () => mockGetPlatform() },
}));
const providerAvailabilityRef = vi.hoisted(() => ({
value: {} as Record<string, 'unknown' | 'checking' | 'available' | 'unavailable'>,
}));
vi.mock('../../../../hooks/use-provider-availability', () => ({
useProviderAvailability: () => providerAvailabilityRef.value,
}));
const mockSetUploadMode = vi.fn();
const mockSetPreferredProvider = vi.fn();
const uploadModeRef = vi.hoisted(() => ({ value: 'random' as 'random' | 'preferred' | 'none' }));
const preferredProviderRef = vi.hoisted(() => ({
value: 'catbox' as 'catbox' | 'imgur',
value: 'catbox' as 'catbox' | 'imgur' | 'imgbb',
}));
vi.mock('../../../../stores/use-media-hosting-store', async (importOriginal) => {
const mod = await importOriginal<typeof import('../../../../stores/use-media-hosting-store')>();
@@ -51,6 +58,7 @@ describe('MediaHostingSettings', () => {
vi.clearAllMocks();
uploadModeRef.value = 'random';
preferredProviderRef.value = 'catbox';
providerAvailabilityRef.value = {};
mockGetPlatform.mockReturnValue('web');
(window as unknown as { electronApi?: unknown }).electronApi = undefined;
container = document.createElement('div');
@@ -99,18 +107,63 @@ describe('MediaHostingSettings', () => {
expect(mockSetUploadMode).toHaveBeenCalledWith('none');
});
it('clicking provider in preferred mode calls setPreferredProvider', async () => {
it('disables unsupported providers on Android', async () => {
uploadModeRef.value = 'preferred';
preferredProviderRef.value = 'catbox';
mockGetPlatform.mockReturnValue('android');
render();
const imgurRadio = container.querySelector<HTMLInputElement>('input[value="imgur"]');
expect(imgurRadio).not.toBeNull();
expect(imgurRadio?.disabled).toBe(false);
expect(imgurRadio?.disabled).toBe(true);
await act(async () => {
imgurRadio?.click();
});
expect(mockSetPreferredProvider).toHaveBeenCalledWith('imgur');
expect(mockSetPreferredProvider).not.toHaveBeenCalled();
});
it('clicking supported provider in preferred mode calls setPreferredProvider', async () => {
uploadModeRef.value = 'preferred';
preferredProviderRef.value = 'imgur';
mockGetPlatform.mockReturnValue('android');
render();
const catboxRadio = container.querySelector<HTMLInputElement>('input[value="catbox"]');
expect(catboxRadio).not.toBeNull();
expect(catboxRadio?.disabled).toBe(false);
await act(async () => {
catboxRadio?.click();
});
expect(mockSetPreferredProvider).toHaveBeenCalledWith('catbox');
});
it('enables ImgBB on Android', async () => {
uploadModeRef.value = 'preferred';
preferredProviderRef.value = 'catbox';
mockGetPlatform.mockReturnValue('android');
render();
const imgbbRadio = container.querySelector<HTMLInputElement>('input[value="imgbb"]');
expect(imgbbRadio).not.toBeNull();
expect(imgbbRadio?.disabled).toBe(false);
await act(async () => {
imgbbRadio?.click();
});
expect(mockSetPreferredProvider).toHaveBeenCalledWith('imgbb');
});
it('disables providers that are unavailable from this network', async () => {
uploadModeRef.value = 'preferred';
preferredProviderRef.value = 'catbox';
providerAvailabilityRef.value = { imgbb: 'unavailable' };
mockGetPlatform.mockReturnValue('android');
render();
const imgbbRadio = container.querySelector<HTMLInputElement>('input[value="imgbb"]');
expect(imgbbRadio).not.toBeNull();
expect(imgbbRadio?.disabled).toBe(true);
expect(imgbbRadio?.closest('label')?.title).toBe('media_hosting_provider_unavailable');
expect(container.textContent).toContain('media_hosting_provider_unavailable');
await act(async () => {
imgbbRadio?.click();
});
expect(mockSetPreferredProvider).not.toHaveBeenCalled();
});
it('clicking Random updates store via setUploadMode', async () => {
@@ -2,7 +2,8 @@ import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import useMediaHostingStore, { MEDIA_HOSTING_PROVIDERS } from '../../../stores/use-media-hosting-store';
import type { ProviderId } from '../../../lib/media-hosting/types';
import { isWebRuntime } from '../../../lib/media-hosting/show-upload-controls';
import { getMediaHostingRuntime } from '../../../lib/media-hosting/show-upload-controls';
import { useProviderAvailability } from '../../../hooks/use-provider-availability';
import styles from '../interface-settings/interface-settings.module.css';
const RADIO_NAME = 'media-hosting-provider';
@@ -14,7 +15,9 @@ const MediaHostingSettings = () => {
const preferredProvider = useMediaHostingStore((state) => state.preferredProvider);
const setUploadMode = useMediaHostingStore((state) => state.setUploadMode);
const setPreferredProvider = useMediaHostingStore((state) => state.setPreferredProvider);
const isWeb = isWebRuntime();
const runtime = getMediaHostingRuntime();
const isWeb = runtime === 'web';
const availability = useProviderAvailability(runtime);
return (
<div className={styles.interfaceSettings}>
@@ -40,25 +43,30 @@ const MediaHostingSettings = () => {
</label>
{uploadMode === 'preferred' && (
<div role='radiogroup' aria-label={t('media_hosting_preferred_provider_label')}>
{MEDIA_HOSTING_PROVIDERS.map((provider) => (
<div key={provider.id} className={styles.setting}>
<label>
<input
type='radio'
name={`${RADIO_NAME}-provider`}
value={provider.id}
checked={preferredProvider === provider.id}
onChange={() => setPreferredProvider(provider.id as ProviderId)}
disabled={isWeb}
/>
{provider.label} (
<a href={provider.homepageUrl} target='_blank' rel='noopener noreferrer'>
{provider.homepageUrl}
</a>
)
</label>
</div>
))}
{MEDIA_HOSTING_PROVIDERS.map((provider) => {
const providerUnavailable = availability[provider.id] === 'unavailable';
const providerDisabled = isWeb || !provider.supportedRuntimes.includes(runtime) || providerUnavailable;
return (
<div key={provider.id} className={styles.setting}>
<label title={providerUnavailable ? t('media_hosting_provider_unavailable') : undefined}>
<input
type='radio'
name={`${RADIO_NAME}-provider`}
value={provider.id}
checked={preferredProvider === provider.id}
onChange={() => setPreferredProvider(provider.id as ProviderId)}
disabled={providerDisabled}
/>
{provider.label} (
<a href={provider.homepageUrl} target='_blank' rel='noopener noreferrer'>
{provider.homepageUrl}
</a>
)
</label>
{providerUnavailable && <div className={styles.settingTip}>{t('media_hosting_provider_unavailable')}</div>}
</div>
);
})}
</div>
)}
</div>
+91 -7
View File
@@ -31,16 +31,28 @@ vi.mock('../../lib/media-hosting/upload-orchestrator', () => ({
orchestrateElectronUpload: vi.fn(),
}));
const providerAvailabilityRef = vi.hoisted(() => ({
value: {} as Record<string, 'unknown' | 'checking' | 'available' | 'unavailable'>,
}));
vi.mock('../../lib/media-hosting/provider-availability', () => ({
ensureProviderAvailability: vi.fn(async () => providerAvailabilityRef.value),
}));
vi.mock('../../lib/media-hosting/provider-order', () => ({
getProviderOrder: vi.fn((opts: { mode: string; preferredProvider: string; runtime: string }) => {
getProviderOrder: vi.fn((opts: { mode: string; preferredProvider: string; runtime: string; availability?: Record<string, string> }) => {
const filterAvailable = (providers: string[]) => providers.filter((provider) => opts.availability?.[provider] !== 'unavailable');
if (opts.mode === 'none') return [];
if (opts.runtime === 'web') return opts.preferredProvider === 'catbox' ? ['catbox'] : [];
return opts.mode === 'preferred' ? [opts.preferredProvider] : ['catbox', 'imgur'];
if (opts.runtime === 'web') return filterAvailable(opts.preferredProvider === 'catbox' ? ['catbox'] : []);
if (opts.runtime === 'android') {
if (opts.mode === 'preferred') return filterAvailable(opts.preferredProvider === 'catbox' || opts.preferredProvider === 'imgbb' ? [opts.preferredProvider] : []);
return filterAvailable(['catbox', 'imgbb']);
}
return filterAvailable(opts.mode === 'preferred' ? [opts.preferredProvider] : ['catbox', 'imgur', 'imgbb']);
}),
}));
const uploadModeRef = vi.hoisted(() => ({ value: 'random' as 'random' | 'preferred' | 'none' }));
const preferredProviderRef = vi.hoisted(() => ({ value: 'catbox' as 'catbox' | 'imgur' }));
const preferredProviderRef = vi.hoisted(() => ({ value: 'catbox' as 'catbox' | 'imgur' | 'imgbb' }));
vi.mock('../../stores/use-media-hosting-store', () => ({
default: (selector: (s: { uploadMode: string; preferredProvider: string }) => unknown) =>
selector({ uploadMode: uploadModeRef.value, preferredProvider: preferredProviderRef.value }),
@@ -89,6 +101,7 @@ describe('useFileUpload', () => {
vi.clearAllMocks();
uploadModeRef.value = 'random';
preferredProviderRef.value = 'catbox';
providerAvailabilityRef.value = {};
latestHook = null;
container = document.createElement('div');
document.body.append(container);
@@ -268,7 +281,7 @@ describe('useFileUpload', () => {
capacitorRejection.data = {
attempts: [
{ provider: 'catbox', error: 'timeout' },
{ provider: 'imgur', error: 'rate limit' },
{ provider: 'imgbb', error: 'rate limit' },
],
};
vi.mocked(FileUploader.pickAndUploadMedia).mockRejectedValue(capacitorRejection);
@@ -278,7 +291,7 @@ describe('useFileUpload', () => {
await hook().handleUpload();
});
expect(window.alert).toHaveBeenCalledWith('upload_failed. upload_failed_all_providers: catbox: timeout; imgur: rate limit');
expect(window.alert).toHaveBeenCalledWith('upload_failed. upload_failed_all_providers: catbox: timeout; imgbb: rate limit');
expect(onUploadComplete).not.toHaveBeenCalled();
expect(hook().isUploading).toBe(false);
});
@@ -319,6 +332,75 @@ describe('useFileUpload', () => {
expect(hook().isUploading).toBe(false);
});
it('does not call Android plugin when preferred provider is unsupported', async () => {
uploadModeRef.value = 'preferred';
preferredProviderRef.value = 'imgur';
vi.mocked(Capacitor.getPlatform).mockReturnValue('android');
const { onUploadComplete, hook } = mountHook();
await act(async () => {
await hook().handleUpload();
});
expect(FileUploader.pickAndUploadMedia).not.toHaveBeenCalled();
expect(window.alert).toHaveBeenCalledWith('upload_failed: imgur is not supported on android. upload_failed_preferred_guidance');
expect(onUploadComplete).not.toHaveBeenCalled();
expect(hook().isUploading).toBe(false);
});
it('does not call Android plugin when preferred provider is unavailable from this network', async () => {
uploadModeRef.value = 'preferred';
preferredProviderRef.value = 'imgbb';
providerAvailabilityRef.value = { imgbb: 'unavailable' };
vi.mocked(Capacitor.getPlatform).mockReturnValue('android');
const { onUploadComplete, hook } = mountHook();
await act(async () => {
await hook().handleUpload();
});
expect(FileUploader.pickAndUploadMedia).not.toHaveBeenCalled();
expect(window.alert).toHaveBeenCalledWith('upload_failed: imgbb is unavailable from this network. upload_failed_preferred_guidance');
expect(onUploadComplete).not.toHaveBeenCalled();
expect(hook().isUploading).toBe(false);
});
it('does not call Android plugin when random mode has no reachable providers', async () => {
uploadModeRef.value = 'random';
preferredProviderRef.value = 'catbox';
providerAvailabilityRef.value = { catbox: 'unavailable', imgbb: 'unavailable' };
vi.mocked(Capacitor.getPlatform).mockReturnValue('android');
const { onUploadComplete, hook } = mountHook();
await act(async () => {
await hook().handleUpload();
});
expect(FileUploader.pickAndUploadMedia).not.toHaveBeenCalled();
expect(window.alert).toHaveBeenCalledWith('upload_failed: No reachable upload providers are available from this network');
expect(onUploadComplete).not.toHaveBeenCalled();
expect(hook().isUploading).toBe(false);
});
it('calls Android plugin with only ImgBB when ImgBB is chosen', async () => {
uploadModeRef.value = 'preferred';
preferredProviderRef.value = 'imgbb';
vi.mocked(Capacitor.getPlatform).mockReturnValue('android');
vi.mocked(FileUploader.pickAndUploadMedia).mockResolvedValue({
url: 'https://i.ibb.co/example/android.png',
fileName: 'android.png',
provider: 'imgbb',
});
const { onUploadComplete, hook } = mountHook();
await act(async () => {
await hook().handleUpload();
});
expect(FileUploader.pickAndUploadMedia).toHaveBeenCalledWith({ providerOrder: ['imgbb'] });
expect(onUploadComplete).toHaveBeenCalledWith('https://i.ibb.co/example/android.png', 'android.png');
});
it('Android call includes provider order payload', async () => {
uploadModeRef.value = 'random';
preferredProviderRef.value = 'catbox';
@@ -340,6 +422,7 @@ describe('useFileUpload', () => {
});
const call = vi.mocked(FileUploader.pickAndUploadMedia).mock.calls[0][0];
expect(call?.providerOrder).toEqual(expect.arrayContaining(['catbox']));
expect(call?.providerOrder).toEqual(expect.arrayContaining(['imgbb']));
expect(onUploadComplete).toHaveBeenCalledWith('https://files.catbox.moe/random.jpg', 'random.jpg');
});
@@ -376,6 +459,7 @@ describe('useFileUpload', () => {
err.attempts = [
{ provider: 'catbox', error: 'timeout' },
{ provider: 'imgur', error: 'rate limit' },
{ provider: 'imgbb', error: 'blocked' },
];
vi.mocked(orchestrateElectronUpload).mockRejectedValue(err);
@@ -391,7 +475,7 @@ describe('useFileUpload', () => {
await uploadPromise;
});
expect(window.alert).toHaveBeenCalledWith('upload_failed. upload_failed_all_providers: catbox: timeout; imgur: rate limit');
expect(window.alert).toHaveBeenCalledWith('upload_failed. upload_failed_all_providers: catbox: timeout; imgur: rate limit; imgbb: blocked');
expect(onUploadComplete).not.toHaveBeenCalled();
expect(hook().isUploading).toBe(false);
});
+23 -16
View File
@@ -1,10 +1,11 @@
import { useCallback, useState } from 'react';
import { Capacitor } from '@capacitor/core';
import { useTranslation } from 'react-i18next';
import FileUploader from '../plugins/file-uploader';
import { formatAggregatedError, formatPreferredModeError, type ProviderAttempt } from '../lib/media-hosting/error-format';
import { getProviderOrder } from '../lib/media-hosting/provider-order';
import { getMediaHostingRuntime, isElectronRuntime } from '../lib/media-hosting/show-upload-controls';
import { orchestrateElectronUpload } from '../lib/media-hosting/upload-orchestrator';
import { ensureProviderAvailability } from '../lib/media-hosting/provider-availability';
import useMediaHostingStore from '../stores/use-media-hosting-store';
import type { ProviderId, UploadAttemptStage } from '../lib/media-hosting/types';
@@ -26,7 +27,7 @@ const ANDROID_STAGE_MAP: Record<string, UploadAttemptStage> = {
const FILE_SELECTION_CANCELLED_ERROR = 'File selection cancelled';
const VALID_PROVIDERS: ProviderId[] = ['catbox', 'imgur'];
const VALID_PROVIDERS: ProviderId[] = ['catbox', 'imgur', 'imgbb'];
/** Raw attempt shape from Android plugin rejection payload */
interface RawAttempt {
@@ -82,16 +83,6 @@ function normalizeAndroidRejection(error: unknown): Error & { attempts?: Provide
return err as Error & { attempts?: ProviderAttempt[] };
}
function isElectronRuntime(): boolean {
return window.electronApi?.isElectron === true || window.isElectron === true;
}
function getRuntime(): 'web' | 'electron' | 'android' {
if (Capacitor.getPlatform() === 'android') return 'android';
if (isElectronRuntime()) return 'electron';
return 'web';
}
interface UseFileUploadOptions {
onUploadComplete: (url: string, fileName: string) => void;
}
@@ -162,14 +153,30 @@ export function useFileUpload(options: UseFileUploadOptions) {
const handleUpload = useCallback(async () => {
if (uploadMode === 'none') return;
const runtime = getRuntime();
const order = getProviderOrder({ mode: uploadMode, preferredProvider, runtime });
const runtime = getMediaHostingRuntime();
try {
setIsUploading(true);
setUploadedFileName(null);
const supportedOrder = getProviderOrder({ mode: uploadMode, preferredProvider, runtime });
const availability = await ensureProviderAvailability(runtime);
const order = getProviderOrder({ mode: uploadMode, preferredProvider, runtime, availability });
if (Capacitor.getPlatform() === 'android') {
if (order.length === 0) {
if (runtime === 'web') {
window.alert(t('upload_not_supported_web'));
return;
}
if (supportedOrder.length > 0) {
const message =
uploadMode === 'preferred' && availability[preferredProvider] === 'unavailable'
? `${preferredProvider} is unavailable from this network`
: 'No reachable upload providers are available from this network';
throw new Error(message);
}
throw new Error(`${preferredProvider} is not supported on ${runtime}`);
}
if (runtime === 'android') {
const result = await FileUploader.pickAndUploadMedia({ providerOrder: order });
if (result.url) {
if (result.fileName) setUploadedFileName(result.fileName);
+7
View File
@@ -0,0 +1,7 @@
import { useSyncExternalStore } from 'react';
import type { MediaHostingRuntime } from '../lib/media-hosting/types';
import { getProviderAvailabilitySnapshot, subscribeProviderAvailability } from '../lib/media-hosting/provider-availability';
export function useProviderAvailability(runtime: MediaHostingRuntime) {
return useSyncExternalStore((listener) => subscribeProviderAvailability(listener, runtime), getProviderAvailabilitySnapshot, getProviderAvailabilitySnapshot);
}
@@ -0,0 +1,59 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { ensureProviderAvailability, getProviderAvailabilitySnapshot, probeProviderAvailability, resetProviderAvailabilityForTests } from '../provider-availability';
class MockImage {
static failedUrlPart = '';
onload: (() => void) | null = null;
onerror: (() => void) | null = null;
referrerPolicy = '';
set src(value: string) {
queueMicrotask(() => {
if (MockImage.failedUrlPart && value.includes(MockImage.failedUrlPart)) {
this.onerror?.();
return;
}
this.onload?.();
});
}
}
describe('provider-availability', () => {
beforeEach(() => {
resetProviderAvailabilityForTests();
MockImage.failedUrlPart = '';
vi.stubGlobal('Image', MockImage);
});
afterEach(() => {
vi.unstubAllGlobals();
resetProviderAvailabilityForTests();
});
it('marks runtime-supported providers available when probes load', async () => {
const snapshot = await ensureProviderAvailability('android');
expect(snapshot.catbox).toBe('available');
expect(snapshot.imgbb).toBe('available');
expect(snapshot.imgur).toBe('unknown');
});
it('marks a provider unavailable when one of its media probes fails', async () => {
MockImage.failedUrlPart = 'i.ibb.co';
const status = await probeProviderAvailability('imgbb');
expect(status).toBe('unavailable');
expect(getProviderAvailabilitySnapshot().imgbb).toBe('unavailable');
});
it('leaves providers unknown when image probing is unavailable', async () => {
vi.stubGlobal('Image', undefined);
const status = await probeProviderAvailability('catbox');
expect(status).toBe('unknown');
expect(getProviderAvailabilitySnapshot().catbox).toBe('unknown');
});
});
@@ -6,24 +6,25 @@ describe('provider-order', () => {
it('returns single-element array with preferred provider', () => {
expect(getPreferredOrder('catbox')).toEqual(['catbox']);
expect(getPreferredOrder('imgur')).toEqual(['imgur']);
expect(getPreferredOrder('imgbb')).toEqual(['imgbb']);
});
});
describe('getRandomOrder', () => {
it('returns shuffled copy (Fisher-Yates) with default rng', () => {
const providers = ['catbox', 'imgur'] as const;
const providers = ['catbox', 'imgur', 'imgbb'] as const;
const result = getRandomOrder(providers);
expect(result).toHaveLength(2);
expect([...result].sort()).toEqual(['catbox', 'imgur']);
expect(result).toHaveLength(3);
expect([...result].sort()).toEqual(['catbox', 'imgbb', 'imgur']);
expect(result).not.toBe(providers);
});
it('uses provided rng for deterministic shuffle', () => {
const providers = ['catbox', 'imgur'] as const;
const providers = ['catbox', 'imgur', 'imgbb'] as const;
const rng = vi.fn().mockReturnValue(0.5);
const result = getRandomOrder(providers, rng);
expect(rng).toHaveBeenCalled();
expect(result).toHaveLength(2);
expect(result).toHaveLength(3);
});
it('handles empty array', () => {
@@ -61,6 +62,13 @@ describe('provider-order', () => {
runtime: 'electron',
}),
).toEqual(['imgur']);
expect(
getProviderOrder({
mode: 'preferred',
preferredProvider: 'imgbb',
runtime: 'android',
}),
).toEqual(['imgbb']);
});
it('returns empty when preferred provider not supported on runtime', () => {
@@ -71,6 +79,20 @@ describe('provider-order', () => {
runtime: 'web',
}),
).toEqual([]);
expect(
getProviderOrder({
mode: 'preferred',
preferredProvider: 'imgur',
runtime: 'android',
}),
).toEqual([]);
expect(
getProviderOrder({
mode: 'preferred',
preferredProvider: 'imgbb',
runtime: 'web',
}),
).toEqual([]);
});
it('returns shuffled list when mode is random', () => {
@@ -79,8 +101,30 @@ describe('provider-order', () => {
preferredProvider: 'catbox',
runtime: 'electron',
});
expect(order).toHaveLength(3);
expect([...order].sort()).toEqual(['catbox', 'imgbb', 'imgur']);
});
it('filters unavailable providers from random mode', () => {
const order = getProviderOrder({
mode: 'random',
preferredProvider: 'catbox',
runtime: 'electron',
availability: { imgur: 'unavailable' },
});
expect(order).toHaveLength(2);
expect([...order].sort()).toEqual(['catbox', 'imgur']);
expect([...order].sort()).toEqual(['catbox', 'imgbb']);
});
it('returns empty when preferred provider is unavailable', () => {
expect(
getProviderOrder({
mode: 'preferred',
preferredProvider: 'imgbb',
runtime: 'android',
availability: { imgbb: 'unavailable' },
}),
).toEqual([]);
});
it('filters by runtime for random mode', () => {
@@ -92,5 +136,15 @@ describe('provider-order', () => {
expect(order).toHaveLength(1);
expect(order).toEqual(['catbox']);
});
it('uses catbox and imgbb for android random mode', () => {
const order = getProviderOrder({
mode: 'random',
preferredProvider: 'catbox',
runtime: 'android',
});
expect(order).toHaveLength(2);
expect([...order].sort()).toEqual(['catbox', 'imgbb']);
});
});
});
@@ -0,0 +1,147 @@
import { MEDIA_HOSTING_PROVIDERS } from './providers';
import type { MediaHostingRuntime, ProviderId } from './types';
export type ProviderAvailabilityStatus = 'unknown' | 'checking' | 'available' | 'unavailable';
export type ProviderAvailabilitySnapshot = Partial<Record<ProviderId, ProviderAvailabilityStatus>>;
const PROBE_TIMEOUT_MS = 5_000;
const PROBE_CACHE_MS = 10 * 60_000;
type Listener = () => void;
interface ProviderAvailabilityEntry {
status: ProviderAvailabilityStatus;
checkedAt: number;
}
const providerById = new Map(MEDIA_HOSTING_PROVIDERS.map((provider) => [provider.id, provider]));
let entries: Record<ProviderId, ProviderAvailabilityEntry> = Object.fromEntries(
MEDIA_HOSTING_PROVIDERS.map((provider) => [provider.id, { status: 'unknown', checkedAt: 0 }]),
) as Record<ProviderId, ProviderAvailabilityEntry>;
let snapshot: ProviderAvailabilitySnapshot = buildSnapshot();
const listeners = new Set<Listener>();
const inFlight = new Map<ProviderId, Promise<ProviderAvailabilityStatus>>();
function buildSnapshot(): ProviderAvailabilitySnapshot {
return Object.fromEntries(MEDIA_HOSTING_PROVIDERS.map((provider) => [provider.id, entries[provider.id]?.status ?? 'unknown'])) as ProviderAvailabilitySnapshot;
}
function emit() {
snapshot = buildSnapshot();
for (const listener of listeners) {
listener();
}
}
function setProviderStatus(provider: ProviderId, status: ProviderAvailabilityStatus) {
entries = {
...entries,
[provider]: {
status,
checkedAt: status === 'checking' ? entries[provider].checkedAt : Date.now(),
},
};
emit();
}
function getRuntimeSupportedProviders(runtime: MediaHostingRuntime): ProviderId[] {
return MEDIA_HOSTING_PROVIDERS.filter((provider) => provider.supportedRuntimes.includes(runtime)).map((provider) => provider.id);
}
function shouldProbeProvider(provider: ProviderId): boolean {
if (inFlight.has(provider)) return false;
const entry = entries[provider];
return entry.status === 'unknown' || Date.now() - entry.checkedAt > PROBE_CACHE_MS;
}
function probeImageUrl(url: string): Promise<boolean> {
return new Promise((resolve) => {
const image = new Image();
let settled = false;
const timeoutId = window.setTimeout(() => settle(false), PROBE_TIMEOUT_MS);
const settle = (available: boolean) => {
if (settled) return;
settled = true;
window.clearTimeout(timeoutId);
image.onload = null;
image.onerror = null;
resolve(available);
};
image.onload = () => settle(true);
image.onerror = () => settle(false);
image.referrerPolicy = 'no-referrer';
image.src = url;
});
}
export function getProviderAvailabilitySnapshot(): ProviderAvailabilitySnapshot {
return snapshot;
}
export function subscribeProviderAvailability(listener: Listener, runtime?: MediaHostingRuntime): () => void {
listeners.add(listener);
if (runtime) {
startProviderAvailabilityProbes(runtime);
}
return () => {
listeners.delete(listener);
};
}
function startProviderAvailabilityProbes(runtime: MediaHostingRuntime) {
for (const provider of getRuntimeSupportedProviders(runtime)) {
if (shouldProbeProvider(provider)) {
void probeProviderAvailability(provider);
}
}
}
export async function probeProviderAvailability(provider: ProviderId): Promise<ProviderAvailabilityStatus> {
const running = inFlight.get(provider);
if (running) return running;
const definition = providerById.get(provider);
if (!definition) return 'unavailable';
if (!shouldProbeProvider(provider)) return entries[provider].status;
const promise = (async () => {
setProviderStatus(provider, 'checking');
if (!definition.availabilityProbeUrls.length) {
setProviderStatus(provider, 'available');
return 'available' as const;
}
if (typeof window === 'undefined' || typeof Image === 'undefined') {
setProviderStatus(provider, 'unknown');
return 'unknown' as const;
}
const results = await Promise.all(definition.availabilityProbeUrls.map((url) => probeImageUrl(url)));
const status: ProviderAvailabilityStatus = results.every(Boolean) ? 'available' : 'unavailable';
setProviderStatus(provider, status);
return status;
})();
inFlight.set(provider, promise);
try {
return await promise;
} finally {
inFlight.delete(provider);
}
}
export async function ensureProviderAvailability(runtime: MediaHostingRuntime): Promise<ProviderAvailabilitySnapshot> {
await Promise.all(getRuntimeSupportedProviders(runtime).map((provider) => probeProviderAvailability(provider)));
return getProviderAvailabilitySnapshot();
}
export function resetProviderAvailabilityForTests() {
entries = Object.fromEntries(MEDIA_HOSTING_PROVIDERS.map((provider) => [provider.id, { status: 'unknown', checkedAt: 0 }])) as Record<
ProviderId,
ProviderAvailabilityEntry
>;
inFlight.clear();
emit();
}
+9 -3
View File
@@ -1,5 +1,6 @@
import type { MediaHostingRuntime, ProviderId, UploadMode } from './types';
import { MEDIA_HOSTING_PROVIDERS } from './providers';
import type { ProviderAvailabilitySnapshot } from './provider-availability';
/** Returns [preferredProvider] for preferred mode */
export function getPreferredOrder(preferredProvider: ProviderId): ProviderId[] {
@@ -22,9 +23,14 @@ function getSupportedProviders(runtime: MediaHostingRuntime): ProviderId[] {
}
/** Get ordered provider list for an upload attempt */
export function getProviderOrder(options: { mode: UploadMode; preferredProvider: ProviderId; runtime: MediaHostingRuntime }): ProviderId[] {
const { mode, preferredProvider, runtime } = options;
const supported = getSupportedProviders(runtime);
export function getProviderOrder(options: {
mode: UploadMode;
preferredProvider: ProviderId;
runtime: MediaHostingRuntime;
availability?: ProviderAvailabilitySnapshot;
}): ProviderId[] {
const { mode, preferredProvider, runtime, availability } = options;
const supported = getSupportedProviders(runtime).filter((provider) => availability?.[provider] !== 'unavailable');
if (mode === 'none') return [];
if (mode === 'preferred') {
@@ -1,13 +1,19 @@
import { Capacitor } from '@capacitor/core';
import type { UploadMode } from './types';
import type { MediaHostingRuntime, UploadMode } from './types';
function isElectronRuntime(): boolean {
export function isElectronRuntime(): boolean {
return window.electronApi?.isElectron === true || window.isElectron === true;
}
export function getMediaHostingRuntime(): MediaHostingRuntime {
if (Capacitor.getPlatform() === 'android') return 'android';
if (isElectronRuntime()) return 'electron';
return 'web';
}
/** Web runtime = web browser, not Electron */
export function isWebRuntime(): boolean {
return Capacitor.getPlatform() === 'web' && !isElectronRuntime();
return getMediaHostingRuntime() === 'web';
}
/**