feat: add daily translation quota of DeepL API for more sustainable service (#1349)
This commit is contained in:
@@ -1,4 +1,7 @@
|
||||
import withPWAInit from '@ducanh2912/next-pwa';
|
||||
import { initOpenNextCloudflareForDev } from '@opennextjs/cloudflare';
|
||||
|
||||
initOpenNextCloudflareForDev();
|
||||
|
||||
const isDev = process.env['NODE_ENV'] === 'development';
|
||||
const appPlatform = process.env['NEXT_PUBLIC_APP_PLATFORM'];
|
||||
|
||||
@@ -186,7 +186,6 @@
|
||||
"Sign in with Apple": "تسجيل الدخول عبر Apple",
|
||||
"Sign in with GitHub": "تسجيل الدخول عبر GitHub",
|
||||
"Account": "الحساب",
|
||||
"Cloud Storage": "التخزين السحابي",
|
||||
"Failed to delete user. Please try again later.": "فشل حذف المستخدم. يرجى المحاولة مرة أخرى لاحقًا.",
|
||||
"Unlimited Offline/Online Reading": "قراءة غير محدودة بدون اتصال/متصلة بالإنترنت",
|
||||
"Unlimited Cloud Sync Devices": "أجهزة مزامنة سحابية غير محدودة",
|
||||
@@ -353,5 +352,9 @@
|
||||
"Show Remaining Time": "إظهار الوقت المتبقي",
|
||||
"Show Page Number": "إظهار رقم الصفحة",
|
||||
"{{time}} min left in chapter": "{{time}} دقيقة متبقية في الفصل",
|
||||
"Override Book Color": "تجاوز لون الكتاب"
|
||||
"Override Book Color": "تجاوز لون الكتاب",
|
||||
"Login Required": "يلزم الدخول",
|
||||
"Quota Exceeded": "تجاوز السعة",
|
||||
"{{percentage}}% of Daily Translation Characters Used.": "تم استخدام {{percentage}}% من أحرف الترجمة اليومية.",
|
||||
"Translation Characters": "أحرف الترجمة"
|
||||
}
|
||||
|
||||
@@ -186,7 +186,6 @@
|
||||
"Sign in with Apple": "Mit Apple anmelden",
|
||||
"Sign in with GitHub": "Mit GitHub anmelden",
|
||||
"Account": "Konto",
|
||||
"Cloud Storage": "Cloud-Speicher",
|
||||
"Failed to delete user. Please try again later.": "Benutzer konnte nicht gelöscht werden. Bitte versuchen Sie es später erneut.",
|
||||
"Unlimited Offline/Online Reading": "Unbegrenztes Offline/Online Lesen",
|
||||
"Unlimited Cloud Sync Devices": "Unbegrenzte Cloud-Synchronisierungsgeräte",
|
||||
@@ -345,5 +344,9 @@
|
||||
"Show Remaining Time": "Verbleibende Zeit anzeigen",
|
||||
"Show Page Number": "Seitenzahl anzeigen",
|
||||
"{{time}} min left in chapter": "{{time}} min verbleibend im Kapitel",
|
||||
"Override Book Color": "Farbe des Buches überschreiben"
|
||||
"Override Book Color": "Farbe des Buches überschreiben",
|
||||
"Login Required": "Anmeldung nötig",
|
||||
"Quota Exceeded": "Limit überschritten",
|
||||
"{{percentage}}% of Daily Translation Characters Used.": "{{percentage}}% der täglichen Übersetzungszeichen verwendet.",
|
||||
"Translation Characters": "Übersetzungszeichen"
|
||||
}
|
||||
|
||||
@@ -186,7 +186,6 @@
|
||||
"Sign in with Apple": "Σύνδεση με Apple",
|
||||
"Sign in with GitHub": "Σύνδεση με GitHub",
|
||||
"Account": "Λογαριασμός",
|
||||
"Cloud Storage": "Αποθηκευτικός χώρος Cloud",
|
||||
"Failed to delete user. Please try again later.": "Αποτυχία διαγραφής χρήστη. Παρακαλώ δοκιμάστε ξανά αργότερα.",
|
||||
"Unlimited Offline/Online Reading": "Απεριόριστη ανάγνωση εκτός/εντός σύνδεσης",
|
||||
"Unlimited Cloud Sync Devices": "Απεριόριστες συσκευές συγχρονισμού Cloud",
|
||||
@@ -345,5 +344,9 @@
|
||||
"Show Remaining Time": "Εμφάνιση υπολειπόμενου χρόνου",
|
||||
"Show Page Number": "Εμφάνιση αριθμού σελίδας",
|
||||
"{{time}} min left in chapter": "{{time}} λεπτά απομένουν στο κεφάλαιο",
|
||||
"Override Book Color": "Παράκαμψη χρώματος βιβλίου"
|
||||
"Override Book Color": "Παράκαμψη χρώματος βιβλίου",
|
||||
"Login Required": "Απαιτείται σύνδεση",
|
||||
"Quota Exceeded": "Υπέρβαση ορίου",
|
||||
"{{percentage}}% of Daily Translation Characters Used.": "{{percentage}}% των ημερήσιων χαρακτήρων μετάφρασης χρησιμοποιήθηκαν.",
|
||||
"Translation Characters": "Χαρακτήρες μετάφρασης"
|
||||
}
|
||||
|
||||
@@ -186,7 +186,6 @@
|
||||
"Sign in with Apple": "Iniciar sesión con Apple",
|
||||
"Sign in with GitHub": "Iniciar sesión con GitHub",
|
||||
"Account": "Cuenta",
|
||||
"Cloud Storage": "Almacenamiento en la nube",
|
||||
"Failed to delete user. Please try again later.": "Error al eliminar usuario. Por favor, inténtelo de nuevo más tarde.",
|
||||
"Unlimited Offline/Online Reading": "Lectura sin límites en línea/sin conexión",
|
||||
"Unlimited Cloud Sync Devices": "Sincronización ilimitada de dispositivos en la nube",
|
||||
@@ -347,5 +346,9 @@
|
||||
"Show Remaining Time": "Mostrar tiempo restante",
|
||||
"Show Page Number": "Mostrar número de página",
|
||||
"{{time}} min left in chapter": "{{time}} min restantes en el capítulo",
|
||||
"Override Book Color": "Sobre escribir color del libro"
|
||||
"Override Book Color": "Sobre escribir color del libro",
|
||||
"Login Required": "Iniciar sesión",
|
||||
"Quota Exceeded": "Límite superado",
|
||||
"{{percentage}}% of Daily Translation Characters Used.": "{{percentage}}% de caracteres de traducción diarios utilizados.",
|
||||
"Translation Characters": "Caracteres de traducción"
|
||||
}
|
||||
|
||||
@@ -186,7 +186,6 @@
|
||||
"Sign in with Apple": "Se connecter avec Apple",
|
||||
"Sign in with GitHub": "Se connecter avec GitHub",
|
||||
"Account": "Compte",
|
||||
"Cloud Storage": "Stockage cloud",
|
||||
"Failed to delete user. Please try again later.": "Échec de la suppression de l'utilisateur. Veuillez réessayer plus tard.",
|
||||
"Unlimited Offline/Online Reading": "Lecture illimitée hors ligne/en ligne",
|
||||
"Unlimited Cloud Sync Devices": "Synchronisation cloud sur appareils illimitée",
|
||||
@@ -347,5 +346,9 @@
|
||||
"Show Remaining Time": "Afficher le temps restant",
|
||||
"Show Page Number": "Afficher le numéro de page",
|
||||
"{{time}} min left in chapter": "{{time}} min restant dans le chapitre",
|
||||
"Override Book Color": "Remplacer la couleur du livre"
|
||||
"Override Book Color": "Remplacer la couleur du livre",
|
||||
"Login Required": "Connexion requise",
|
||||
"Quota Exceeded": "Quota dépassé",
|
||||
"{{percentage}}% of Daily Translation Characters Used.": "{{percentage}}% des caractères de traduction quotidiens utilisés.",
|
||||
"Translation Characters": "Caractères de traduction"
|
||||
}
|
||||
|
||||
@@ -186,7 +186,6 @@
|
||||
"Sign in with Apple": "Apple के साथ साइन इन करें",
|
||||
"Sign in with GitHub": "GitHub के साथ साइन इन करें",
|
||||
"Account": "खाता",
|
||||
"Cloud Storage": "क्लाउड स्टोरेज",
|
||||
"Failed to delete user. Please try again later.": "उपयोगकर्ता को हटाने में विफल। कृपया बाद में पुनः प्रयास करें।",
|
||||
"Unlimited Offline/Online Reading": "असीमित ऑफलाइन/ऑनलाइन पढ़ना",
|
||||
"Unlimited Cloud Sync Devices": "असीमित क्लाउड सिंक उपकरण",
|
||||
@@ -345,5 +344,9 @@
|
||||
"Show Remaining Time": "शेष समय दिखाएं",
|
||||
"Show Page Number": "पृष्ठ संख्या दिखाएं",
|
||||
"{{time}} min left in chapter": "{{time}} मिनट शेष अध्याय में",
|
||||
"Override Book Color": "पुस्तक रंग ओवरराइड करें"
|
||||
"Override Book Color": "पुस्तक रंग ओवरराइड करें",
|
||||
"Login Required": "लॉगिन ज़रूरी",
|
||||
"Quota Exceeded": "सीमा पार",
|
||||
"{{percentage}}% of Daily Translation Characters Used.": "{{percentage}}% दैनिक अनुवाद वर्णों का उपयोग किया गया है।",
|
||||
"Translation Characters": "अनुवाद वर्ण"
|
||||
}
|
||||
|
||||
@@ -186,7 +186,6 @@
|
||||
"Sign in with Apple": "Masuk dengan Apple",
|
||||
"Sign in with GitHub": "Masuk dengan GitHub",
|
||||
"Account": "Akun",
|
||||
"Cloud Storage": "Penyimpanan Cloud",
|
||||
"Failed to delete user. Please try again later.": "Gagal menghapus pengguna. Silakan coba lagi nanti.",
|
||||
"Unlimited Offline/Online Reading": "Membaca Offline/Online Tanpa Batas",
|
||||
"Unlimited Cloud Sync Devices": "Perangkat Sinkronisasi Cloud Tanpa Batas",
|
||||
@@ -343,5 +342,9 @@
|
||||
"Show Remaining Time": "Perlihatkan Waktu Tersisa",
|
||||
"Show Page Number": "Perlihatkan Nomor Halaman",
|
||||
"{{time}} min left in chapter": "{{time}} menit tersisa di bab",
|
||||
"Override Book Color": "Ubah Warna Buku"
|
||||
"Override Book Color": "Ubah Warna Buku",
|
||||
"Login Required": "Perlu masuk",
|
||||
"Quota Exceeded": "Kuota terlampaui",
|
||||
"{{percentage}}% of Daily Translation Characters Used.": "{{percentage}}% dari Karakter Terjemahan Harian Digunakan.",
|
||||
"Translation Characters": "Karakter Terjemahan"
|
||||
}
|
||||
|
||||
@@ -186,7 +186,6 @@
|
||||
"Sign in with Apple": "Accedi con Apple",
|
||||
"Sign in with GitHub": "Accedi con GitHub",
|
||||
"Account": "Account",
|
||||
"Cloud Storage": "Archiviazione Cloud",
|
||||
"Failed to delete user. Please try again later.": "Impossibile eliminare l'utente. Riprova più tardi.",
|
||||
"Unlimited Offline/Online Reading": "Lettura offline/online illimitata",
|
||||
"Unlimited Cloud Sync Devices": "Sincronizzazione cloud su dispositivi illimitati",
|
||||
@@ -347,5 +346,9 @@
|
||||
"Show Remaining Time": "Mostra tempo rimanente",
|
||||
"Show Page Number": "Mostra numero pagina",
|
||||
"{{time}} min left in chapter": "{{time}} min rimasti nel capitolo",
|
||||
"Override Book Color": "Override colore libro"
|
||||
"Override Book Color": "Override colore libro",
|
||||
"Login Required": "Accesso richiesto",
|
||||
"Quota Exceeded": "Limite superato",
|
||||
"{{percentage}}% of Daily Translation Characters Used.": "{{percentage}}% dei caratteri di traduzione giornalieri utilizzati.",
|
||||
"Translation Characters": "Caratteri di traduzione"
|
||||
}
|
||||
|
||||
@@ -186,7 +186,6 @@
|
||||
"Sign in with Apple": "Appleでサインイン",
|
||||
"Sign in with GitHub": "GitHubでサインイン",
|
||||
"Account": "アカウント",
|
||||
"Cloud Storage": "クラウドストレージ",
|
||||
"Failed to delete user. Please try again later.": "ユーザーの削除に失敗しました。後ほど再試行してください。",
|
||||
"Unlimited Offline/Online Reading": "無制限のオフライン/オンライン読書",
|
||||
"Unlimited Cloud Sync Devices": "無制限のクラウド同期デバイス",
|
||||
@@ -343,5 +342,9 @@
|
||||
"Show Remaining Time": "残り時間を表示",
|
||||
"Show Page Number": "ページ番号を表示",
|
||||
"{{time}} min left in chapter": "{{time}}分残り",
|
||||
"Override Book Color": "書籍の色を上書き"
|
||||
"Override Book Color": "書籍の色を上書き",
|
||||
"Login Required": "ログイン必須",
|
||||
"Quota Exceeded": "上限超過",
|
||||
"{{percentage}}% of Daily Translation Characters Used.": "日次翻訳文字数の使用量は{{percentage}}%です。",
|
||||
"Translation Characters": "翻訳文字数"
|
||||
}
|
||||
|
||||
@@ -186,7 +186,6 @@
|
||||
"Sign in with Apple": "Apple로 로그인",
|
||||
"Sign in with GitHub": "GitHub로 로그인",
|
||||
"Account": "계정",
|
||||
"Cloud Storage": "클라우드 스토리지",
|
||||
"Failed to delete user. Please try again later.": "사용자 삭제에 실패했습니다. 나중에 다시 시도해 주세요.",
|
||||
"Unlimited Offline/Online Reading": "무제한 오프라인/온라인 읽기",
|
||||
"Unlimited Cloud Sync Devices": "무제한 클라우드 동기화 기기",
|
||||
@@ -343,5 +342,9 @@
|
||||
"Show Remaining Time": "남은 시간 표시",
|
||||
"Show Page Number": "페이지 번호 표시",
|
||||
"{{time}} min left in chapter": "{{time}}분 남음",
|
||||
"Override Book Color": "책 색상 덮어쓰기"
|
||||
"Override Book Color": "책 색상 덮어쓰기",
|
||||
"Login Required": "로그인 필요",
|
||||
"Quota Exceeded": "한도 초과",
|
||||
"{{percentage}}% of Daily Translation Characters Used.": "일일 번역 문자 사용량 {{percentage}}%.",
|
||||
"Translation Characters": "번역 문자"
|
||||
}
|
||||
|
||||
@@ -228,7 +228,6 @@
|
||||
"Slow": "Langzaam",
|
||||
"Fast": "Snel",
|
||||
"Reading Progress Synced": "Leesvoortgang gesynchroniseerd",
|
||||
"Cloud Storage": "Cloudopslag",
|
||||
"Failed to delete user. Please try again later.": "Gebruiker verwijderen mislukt. Probeer het later opnieuw.",
|
||||
"Free Tier": "Gratis niveau",
|
||||
"Community Support": "Community-ondersteuning",
|
||||
@@ -345,5 +344,9 @@
|
||||
"Show Remaining Time": "Toon resterende tijd",
|
||||
"Show Page Number": "Toon paginanummer",
|
||||
"{{time}} min left in chapter": "Nog {{time}} min in hoofdstuk",
|
||||
"Override Book Color": "Boekkleur overschrijven"
|
||||
"Override Book Color": "Boekkleur overschrijven",
|
||||
"Login Required": "Inloggen vereist",
|
||||
"Quota Exceeded": "Limiet overschreden",
|
||||
"{{percentage}}% of Daily Translation Characters Used.": "{{percentage}}% van de dagelijkse vertaaltekens gebruikt.",
|
||||
"Translation Characters": "Vertaaltekens"
|
||||
}
|
||||
|
||||
@@ -186,7 +186,6 @@
|
||||
"Sign in with Apple": "Zaloguj się za pomocą Apple",
|
||||
"Sign in with GitHub": "Zaloguj się za pomocą GitHub",
|
||||
"Account": "Konto",
|
||||
"Cloud Storage": "Przestrzeń w chmurze",
|
||||
"Failed to delete user. Please try again later.": "Nie udało się usunąć użytkownika. Spróbuj ponownie później.",
|
||||
"Unlimited Offline/Online Reading": "Nieograniczone czytanie offline/online",
|
||||
"Unlimited Cloud Sync Devices": "Nieograniczona liczba urządzeń synchronizowanych w chmurze",
|
||||
@@ -349,5 +348,9 @@
|
||||
"Show Remaining Time": "Pokazuj pozostały czas",
|
||||
"Show Page Number": "Pokaż numer strony",
|
||||
"{{time}} min left in chapter": "{{time}} min pozostało do końca rozdziału",
|
||||
"Override Book Color": "Nadpisz kolor książki"
|
||||
"Override Book Color": "Nadpisz kolor książki",
|
||||
"Login Required": "Wymagane logowanie",
|
||||
"Quota Exceeded": "Limit przekroczony",
|
||||
"{{percentage}}% of Daily Translation Characters Used.": "{{percentage}}% użytych znaków tłumaczenia dziennego.",
|
||||
"Translation Characters": "Znaki tłumaczenia"
|
||||
}
|
||||
|
||||
@@ -186,7 +186,6 @@
|
||||
"Sign in with Apple": "Entrar com o Apple",
|
||||
"Sign in with GitHub": "Entrar com o GitHub",
|
||||
"Account": "Conta",
|
||||
"Cloud Storage": "Armazenamento na nuvem",
|
||||
"Failed to delete user. Please try again later.": "Falha ao excluir usuário. Por favor, tente novamente mais tarde.",
|
||||
"Unlimited Offline/Online Reading": "Leitura offline/online ilimitada",
|
||||
"Unlimited Cloud Sync Devices": "Sincronização ilimitada de dispositivos na nuvem",
|
||||
@@ -347,5 +346,9 @@
|
||||
"Show Remaining Time": "Mostrar Tempo Restante",
|
||||
"Show Page Number": "Mostrar Número da Página",
|
||||
"{{time}} min left in chapter": "{{time}} min restantes no capítulo",
|
||||
"Override Book Color": "Substituir Cor do Livro"
|
||||
"Override Book Color": "Substituir Cor do Livro",
|
||||
"Login Required": "Login necessário",
|
||||
"Quota Exceeded": "Limite excedido",
|
||||
"{{percentage}}% of Daily Translation Characters Used.": "{{percentage}}% dos caracteres de tradução diários usados.",
|
||||
"Translation Characters": "Caracteres de Tradução"
|
||||
}
|
||||
|
||||
@@ -186,7 +186,6 @@
|
||||
"Sign in with Apple": "Войти через Apple",
|
||||
"Sign in with GitHub": "Войти через GitHub",
|
||||
"Account": "Аккаунт",
|
||||
"Cloud Storage": "Облачное хранилище",
|
||||
"Failed to delete user. Please try again later.": "Не удалось удалить пользователя. Пожалуйста, повторите попытку позже.",
|
||||
"Unlimited Offline/Online Reading": "Безлимитное чтение офлайн/онлайн",
|
||||
"Unlimited Cloud Sync Devices": "Неограниченное количество устройств для облачной синхронизации",
|
||||
@@ -349,5 +348,9 @@
|
||||
"Show Remaining Time": "Показать оставшееся время",
|
||||
"Show Page Number": "Показать номер страницы",
|
||||
"{{time}} min left in chapter": "{{time}} мин осталось в главе",
|
||||
"Override Book Color": "Переопределить цвет книги"
|
||||
"Override Book Color": "Переопределить цвет книги",
|
||||
"Login Required": "Требуется вход",
|
||||
"Quota Exceeded": "Лимит превышен",
|
||||
"{{percentage}}% of Daily Translation Characters Used.": "Использовано {{percentage}}% от суточного лимита символов перевода.",
|
||||
"Translation Characters": "Символы перевода"
|
||||
}
|
||||
|
||||
@@ -186,7 +186,6 @@
|
||||
"Sign in with Apple": "Apple ile giriş yap",
|
||||
"Sign in with GitHub": "GitHub ile giriş yap",
|
||||
"Account": "Hesap",
|
||||
"Cloud Storage": "Bulut Depolama",
|
||||
"Failed to delete user. Please try again later.": "Kullanıcı silinemedi. Lütfen daha sonra tekrar deneyin.",
|
||||
"Unlimited Offline/Online Reading": "Sınırsız Çevrimdışı/Çevrimiçi Okuma",
|
||||
"Unlimited Cloud Sync Devices": "Sınırsız Bulut Senkronizasyon Cihazları",
|
||||
@@ -345,5 +344,9 @@
|
||||
"Show Remaining Time": "Kalan Süreyi Göster",
|
||||
"Show Page Number": "Sayfa Numarasını Göster",
|
||||
"{{time}} min left in chapter": "{{time}} dakika bölümde kaldı",
|
||||
"Override Book Color": "Kitap Rengini Geçersiz Kıl"
|
||||
"Override Book Color": "Kitap Rengini Geçersiz Kıl",
|
||||
"Login Required": "Giriş gerekli",
|
||||
"Quota Exceeded": "Kota aşıldı",
|
||||
"{{percentage}}% of Daily Translation Characters Used.": "Çeviri için günlük karakterlerin %{{percentage}}'si kullanıldı.",
|
||||
"Translation Characters": "Çeviri Karakterleri"
|
||||
}
|
||||
|
||||
@@ -186,7 +186,6 @@
|
||||
"Sign in with Apple": "Увійти через Apple",
|
||||
"Sign in with GitHub": "Увійти через GitHub",
|
||||
"Account": "Обліковий запис",
|
||||
"Cloud Storage": "Хмарне сховище",
|
||||
"Failed to delete user. Please try again later.": "Не вдалося видалити користувача. Будь ласка, спробуйте пізніше.",
|
||||
"Unlimited Offline/Online Reading": "Необмежене читання офлайн/онлайн",
|
||||
"Unlimited Cloud Sync Devices": "Необмежена кількість пристроїв для хмарної синхронізації",
|
||||
@@ -349,5 +348,9 @@
|
||||
"Show Remaining Time": "Показати залишок часу",
|
||||
"Show Page Number": "Показати номер сторінки",
|
||||
"{{time}} min left in chapter": "{{time}} хв до кінця розділу",
|
||||
"Override Book Color": "Перевизначити колір книги"
|
||||
"Override Book Color": "Перевизначити колір книги",
|
||||
"Login Required": "Потрібен вхід",
|
||||
"Quota Exceeded": "Перевищено ліміт",
|
||||
"{{percentage}}% of Daily Translation Characters Used.": "Використано {{percentage}}% символів перекладу за день.",
|
||||
"Translation Characters": "Символи перекладу"
|
||||
}
|
||||
|
||||
@@ -186,7 +186,6 @@
|
||||
"Sign in with Apple": "Đăng nhập với Apple",
|
||||
"Sign in with GitHub": "Đăng nhập với GitHub",
|
||||
"Account": "Tài khoản",
|
||||
"Cloud Storage": "Lưu trữ đám mây",
|
||||
"Failed to delete user. Please try again later.": "Không thể xóa người dùng. Vui lòng thử lại sau.",
|
||||
"Unlimited Offline/Online Reading": "Đọc ngoại tuyến/trực tuyến không giới hạn",
|
||||
"Unlimited Cloud Sync Devices": "Đồng bộ hóa thiết bị đám mây không giới hạn",
|
||||
@@ -343,5 +342,9 @@
|
||||
"Show Remaining Time": "Hiển thị thời gian còn lại",
|
||||
"Show Page Number": "Hiển thị số trang",
|
||||
"{{time}} min left in chapter": "{{time}} phút còn lại trong chương",
|
||||
"Override Book Color": "Đổi màu sách"
|
||||
"Override Book Color": "Đổi màu sách",
|
||||
"Login Required": "Cần đăng nhập",
|
||||
"Quota Exceeded": "Vượt hạn mức",
|
||||
"{{percentage}}% of Daily Translation Characters Used.": "{{percentage}}% số ký tự dịch hàng ngày đã sử dụng.",
|
||||
"Translation Characters": "Những ký tự dịch"
|
||||
}
|
||||
|
||||
@@ -186,7 +186,6 @@
|
||||
"Sign in with Apple": "使用 Apple 登录",
|
||||
"Sign in with GitHub": "使用 GitHub 登录",
|
||||
"Account": "账户",
|
||||
"Cloud Storage": "云存储",
|
||||
"Failed to delete user. Please try again later.": "删除用户失败。请稍后重试。",
|
||||
"Unlimited Offline/Online Reading": "无限离线和在线阅读",
|
||||
"Unlimited Cloud Sync Devices": "无限云同步设备个数",
|
||||
@@ -343,5 +342,9 @@
|
||||
"Show Remaining Time": "显示剩余时间",
|
||||
"Show Page Number": "显示页码",
|
||||
"{{time}} min left in chapter": "本章剩余 {{time}} 分钟",
|
||||
"Override Book Color": "覆盖书籍颜色"
|
||||
"Override Book Color": "覆盖书籍颜色",
|
||||
"Login Required": "需登录",
|
||||
"Quota Exceeded": "额度不足",
|
||||
"{{percentage}}% of Daily Translation Characters Used.": "已使用 {{percentage}}% 的每日翻译字符数",
|
||||
"Translation Characters": "翻译字数"
|
||||
}
|
||||
|
||||
@@ -186,7 +186,6 @@
|
||||
"Sign in with Apple": "使用 Apple 登入",
|
||||
"Sign in with GitHub": "使用 GitHub 登入",
|
||||
"Account": "帳戶",
|
||||
"Cloud Storage": "雲端儲存空間",
|
||||
"Failed to delete user. Please try again later.": "刪除使用者失敗。請稍後再試。",
|
||||
"Unlimited Offline/Online Reading": "無限離線/線上閱讀",
|
||||
"Unlimited Cloud Sync Devices": "無限雲端同步裝置",
|
||||
@@ -343,5 +342,9 @@
|
||||
"Show Remaining Time": "顯示剩餘時間",
|
||||
"Show Page Number": "顯示頁碼",
|
||||
"{{time}} min left in chapter": "本章剩餘 {{time}} 分鐘",
|
||||
"Override Book Color": "覆蓋書籍顏色"
|
||||
"Override Book Color": "覆蓋書籍顏色",
|
||||
"Login Required": "需登入",
|
||||
"Quota Exceeded": "額度不足",
|
||||
"{{percentage}}% of Daily Translation Characters Used.": "已使用 {{percentage}}% 的每日翻譯字數",
|
||||
"Translation Characters": "翻譯字數"
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import clsx from 'clsx';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { PiUserCircle } from 'react-icons/pi';
|
||||
import { PiUserCircleCheck } from 'react-icons/pi';
|
||||
@@ -13,14 +13,13 @@ import { DOWNLOAD_READEST_URL } from '@/services/constants';
|
||||
import { useAuth } from '@/context/AuthContext';
|
||||
import { useEnv } from '@/context/EnvContext';
|
||||
import { useThemeStore } from '@/store/themeStore';
|
||||
import { useQuotaStats } from '@/hooks/useQuotaStats';
|
||||
import { useSettingsStore } from '@/store/settingsStore';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { useResponsiveSize } from '@/hooks/useResponsiveSize';
|
||||
import { getStoragePlanData } from '@/utils/access';
|
||||
import { navigateToLogin, navigateToProfile } from '@/utils/nav';
|
||||
import { tauriHandleSetAlwaysOnTop, tauriHandleToggleFullScreen } from '@/utils/window';
|
||||
import { optInTelemetry, optOutTelemetry } from '@/utils/telemetry';
|
||||
import { QuotaType } from '@/types/user';
|
||||
import UserAvatar from '@/components/UserAvatar';
|
||||
import MenuItem from '@/components/MenuItem';
|
||||
import Quota from '@/components/Quota';
|
||||
@@ -33,10 +32,9 @@ const SettingsMenu: React.FC<SettingsMenuProps> = ({ setIsDropdownOpen }) => {
|
||||
const _ = useTranslation();
|
||||
const router = useRouter();
|
||||
const { envConfig, appService } = useEnv();
|
||||
const { token, user } = useAuth();
|
||||
const { user } = useAuth();
|
||||
const { themeMode, setThemeMode } = useThemeStore();
|
||||
const { settings, setSettings, saveSettings } = useSettingsStore();
|
||||
const [quotas, setQuotas] = React.useState<QuotaType[]>([]);
|
||||
const [isAutoUpload, setIsAutoUpload] = useState(settings.autoUpload);
|
||||
const [isAutoCheckUpdates, setIsAutoCheckUpdates] = useState(settings.autoCheckUpdates);
|
||||
const [isAlwaysOnTop, setIsAlwaysOnTop] = useState(settings.alwaysOnTop);
|
||||
@@ -48,6 +46,8 @@ const SettingsMenu: React.FC<SettingsMenuProps> = ({ setIsDropdownOpen }) => {
|
||||
const [isTelemetryEnabled, setIsTelemetryEnabled] = useState(settings.telemetryEnabled);
|
||||
const iconSize = useResponsiveSize(16);
|
||||
|
||||
const { quotas } = useQuotaStats();
|
||||
|
||||
const showAboutReadest = () => {
|
||||
setAboutDialogVisible(true);
|
||||
setIsDropdownOpen?.(false);
|
||||
@@ -143,22 +143,6 @@ const SettingsMenu: React.FC<SettingsMenuProps> = ({ setIsDropdownOpen }) => {
|
||||
setIsTelemetryEnabled(settings.telemetryEnabled);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!user || !token) return;
|
||||
const storagPlan = getStoragePlanData(token);
|
||||
const storageQuota: QuotaType = {
|
||||
name: _('Storage'),
|
||||
tooltip: _('{{percentage}}% of Cloud Storage Used.', {
|
||||
percentage: Math.round((storagPlan.usage / storagPlan.quota) * 100),
|
||||
}),
|
||||
used: Math.round(storagPlan.usage / 1024 / 1024),
|
||||
total: Math.round(storagPlan.quota / 1024 / 1024),
|
||||
unit: 'MB',
|
||||
};
|
||||
setQuotas([storageQuota]);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [token]);
|
||||
|
||||
const avatarUrl = user?.user_metadata?.['picture'] || user?.user_metadata?.['avatar_url'];
|
||||
const userFullName = user?.user_metadata?.['full_name'];
|
||||
const userDisplayName = userFullName ? userFullName.split(' ')[0] : null;
|
||||
@@ -188,7 +172,7 @@ const SettingsMenu: React.FC<SettingsMenuProps> = ({ setIsDropdownOpen }) => {
|
||||
}
|
||||
>
|
||||
<ul>
|
||||
<Quota quotas={quotas} className='h-10 pl-3 pr-2' />
|
||||
<Quota quotas={quotas} labelClassName='h-10 pl-3 pr-2' />
|
||||
<MenuItem label={_('Account')} noIcon onClick={handleUserProfile} />
|
||||
</ul>
|
||||
</MenuItem>
|
||||
|
||||
@@ -6,7 +6,7 @@ import { useSettingsStore } from '@/store/settingsStore';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { useTranslator } from '@/hooks/useTranslator';
|
||||
import { TRANSLATED_LANGS } from '@/services/constants';
|
||||
import { UseTranslatorOptions } from '@/services/translators';
|
||||
import { UseTranslatorOptions, getTranslators } from '@/services/translators';
|
||||
import { localeToLang } from '@/utils/lang';
|
||||
import Select from '@/components/Select';
|
||||
|
||||
@@ -76,7 +76,12 @@ const TranslatorPopup: React.FC<TranslatorPopupProps> = ({
|
||||
};
|
||||
|
||||
const handleProviderChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
const selectedTranslator = translators.find((t) => t.name === event.target.value);
|
||||
const requestedProvider = event.target.value;
|
||||
const availableTranslators = getTranslators().filter(
|
||||
(t) => (t.authRequired ? !!token : true) && !t.quotaExceeded,
|
||||
);
|
||||
const selectedTranslator =
|
||||
availableTranslators.find((t) => t.name === requestedProvider) || availableTranslators[0]!;
|
||||
if (selectedTranslator) {
|
||||
settings.globalReadSettings.translationProvider = selectedTranslator.name;
|
||||
setSettings(settings);
|
||||
@@ -86,9 +91,16 @@ const TranslatorPopup: React.FC<TranslatorPopupProps> = ({
|
||||
|
||||
useEffect(() => {
|
||||
const availableProviders = translators.map((t) => {
|
||||
return { name: t.name, label: t.label };
|
||||
let label = t.label;
|
||||
if (t.authRequired && !token) {
|
||||
label = `${label} (${_('Login Required')})`;
|
||||
} else if (t.quotaExceeded) {
|
||||
label = `${label} (${_('Quota Exceeded')})`;
|
||||
}
|
||||
return { name: t.name, label };
|
||||
});
|
||||
setProviders(availableProviders);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [translators]);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -2,6 +2,7 @@ import clsx from 'clsx';
|
||||
import i18n from 'i18next';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useEnv } from '@/context/EnvContext';
|
||||
import { useAuth } from '@/context/AuthContext';
|
||||
import { useReaderStore } from '@/store/readerStore';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { saveViewSettings } from '../../utils/viewSettingsHelper';
|
||||
@@ -11,6 +12,7 @@ import Select from '@/components/Select';
|
||||
|
||||
const LangPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
const _ = useTranslation();
|
||||
const { token } = useAuth();
|
||||
const { envConfig } = useEnv();
|
||||
const { getViewSettings, setViewSettings } = useReaderStore();
|
||||
const viewSettings = getViewSettings(bookKey)!;
|
||||
@@ -47,15 +49,27 @@ const LangPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
const getTranslationProviderOptions = () => {
|
||||
const translators = getTranslators();
|
||||
const availableProviders = translators.map((t) => {
|
||||
return { value: t.name, label: t.label };
|
||||
let label = t.label;
|
||||
if (t.authRequired && !token) {
|
||||
label = `${label} (${_('Login Required')})`;
|
||||
} else if (t.quotaExceeded) {
|
||||
label = `${label} (${_('Quota Exceeded')})`;
|
||||
}
|
||||
return { value: t.name, label };
|
||||
});
|
||||
return availableProviders;
|
||||
};
|
||||
|
||||
const getCurrentTranslationProviderOption = () => {
|
||||
const value = translationProvider;
|
||||
const availableProviders = getTranslationProviderOptions();
|
||||
return availableProviders.find((p) => p.value === value) || availableProviders[0]!;
|
||||
const allProviders = getTranslationProviderOptions();
|
||||
const availableTranslators = getTranslators().filter(
|
||||
(t) => (t.authRequired ? !!token : true) && !t.quotaExceeded,
|
||||
);
|
||||
const currentProvider = availableTranslators.find((t) => t.name === value)
|
||||
? value
|
||||
: availableTranslators[0]?.name;
|
||||
return allProviders.find((p) => p.value === currentProvider) || allProviders[0]!;
|
||||
};
|
||||
|
||||
const handleSelectTranslationProvider = (event: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
'use client';
|
||||
|
||||
import clsx from 'clsx';
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import React, { useState, useRef } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { IoArrowBack } from 'react-icons/io5';
|
||||
import { PiUserCircle } from 'react-icons/pi';
|
||||
import { useEnv } from '@/context/EnvContext';
|
||||
import { useAuth } from '@/context/AuthContext';
|
||||
import { useTheme } from '@/hooks/useTheme';
|
||||
import { useQuotaStats } from '@/hooks/useQuotaStats';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { useSettingsStore } from '@/store/settingsStore';
|
||||
import { useTrafficLightStore } from '@/store/trafficLightStore';
|
||||
import { QuotaType, UserPlan } from '@/types/user';
|
||||
import { getStoragePlanData, getUserPlan } from '@/utils/access';
|
||||
import { UserPlan } from '@/types/user';
|
||||
import { navigateToLibrary } from '@/utils/nav';
|
||||
import { deleteUser } from '@/libs/user';
|
||||
import { eventDispatcher } from '@/utils/event';
|
||||
@@ -28,36 +28,13 @@ const ProfilePage = () => {
|
||||
const { token, user, logout } = useAuth();
|
||||
const { isTrafficLightVisible } = useTrafficLightStore();
|
||||
const { settings, setSettings, saveSettings } = useSettingsStore();
|
||||
const [userPlan, setUserPlan] = useState<UserPlan>('free');
|
||||
const [quotas, setQuotas] = React.useState<QuotaType[]>([]);
|
||||
const [showConfirmDelete, setShowConfirmDelete] = useState(false);
|
||||
|
||||
const headerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useTheme({ systemUIVisible: false });
|
||||
|
||||
useEffect(() => {
|
||||
if (!user || !token) return;
|
||||
|
||||
try {
|
||||
const userPlan = getUserPlan(token);
|
||||
const storagePlan = getStoragePlanData(token);
|
||||
const storageQuota = {
|
||||
name: _('Cloud Storage'),
|
||||
tooltip: _('{{percentage}}% of Cloud Storage Used.', {
|
||||
percentage: Math.round((storagePlan.usage / storagePlan.quota) * 100),
|
||||
}),
|
||||
used: Math.round(storagePlan.usage / 1024 / 1024),
|
||||
total: Math.round(storagePlan.quota / 1024 / 1024),
|
||||
unit: 'MB',
|
||||
};
|
||||
setUserPlan(userPlan);
|
||||
setQuotas([storageQuota]);
|
||||
} catch (error) {
|
||||
console.error('Error loading user plan data:', error);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [token]);
|
||||
const { quotas, userPlan } = useQuotaStats();
|
||||
|
||||
const handleGoBack = () => {
|
||||
navigateToLibrary(router);
|
||||
@@ -240,10 +217,15 @@ const ProfilePage = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='bg-base-300 mb-8 rounded-lg'>
|
||||
<div className='mb-8 rounded-lg'>
|
||||
<div className='p-0'>
|
||||
{quotas && quotas.length > 0 ? (
|
||||
<Quota quotas={quotas} showProgress className='h-10 pl-4 pr-2' />
|
||||
<Quota
|
||||
quotas={quotas}
|
||||
showProgress
|
||||
className='space-y-4'
|
||||
labelClassName='pl-4 pr-2'
|
||||
/>
|
||||
) : (
|
||||
<div className='h-10 animate-pulse'></div>
|
||||
)}
|
||||
|
||||
@@ -10,12 +10,13 @@ type QuotaProps = {
|
||||
unit: string;
|
||||
}[];
|
||||
className?: string;
|
||||
labelClassName?: string;
|
||||
showProgress?: boolean;
|
||||
};
|
||||
|
||||
const Quota: React.FC<QuotaProps> = ({ quotas, showProgress, className }) => {
|
||||
const Quota: React.FC<QuotaProps> = ({ quotas, showProgress, className, labelClassName }) => {
|
||||
return (
|
||||
<div className={clsx('text-base-content w-full space-y-2 rounded-md text-base sm:text-sm')}>
|
||||
<div className={clsx('text-base-content w-full rounded-md text-base sm:text-sm', className)}>
|
||||
{quotas.map((quota) => {
|
||||
const usagePercentage = (quota.used / quota.total) * 100;
|
||||
let bgColor = 'bg-green-500';
|
||||
@@ -30,7 +31,7 @@ const Quota: React.FC<QuotaProps> = ({ quotas, showProgress, className }) => {
|
||||
key={quota.name}
|
||||
className={clsx(
|
||||
'relative w-full overflow-hidden rounded-md',
|
||||
showProgress && 'border-base-300 border',
|
||||
showProgress && 'bg-base-300',
|
||||
)}
|
||||
>
|
||||
{showProgress && (
|
||||
@@ -41,9 +42,12 @@ const Quota: React.FC<QuotaProps> = ({ quotas, showProgress, className }) => {
|
||||
)}
|
||||
|
||||
<div
|
||||
className={clsx('relative flex items-center justify-between gap-4 p-2', className)}
|
||||
className={clsx(
|
||||
'relative flex items-center justify-between gap-4 p-2',
|
||||
labelClassName,
|
||||
)}
|
||||
>
|
||||
<div className='lg:tooltip lg:tooltip-bottom' data-tip={quota.tooltip}>
|
||||
<div className='lg:tooltip lg:tooltip-right' data-tip={quota.tooltip}>
|
||||
<span className='truncate'>{quota.name}</span>
|
||||
</div>
|
||||
<div className='text-right text-xs'>
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useAuth } from '@/context/AuthContext';
|
||||
import { QuotaType, UserPlan } from '@/types/user';
|
||||
import { getStoragePlanData, getTranslationPlanData, getUserPlan } from '@/utils/access';
|
||||
import { useTranslation } from './useTranslation';
|
||||
|
||||
export const useQuotaStats = () => {
|
||||
const _ = useTranslation();
|
||||
const { token, user } = useAuth();
|
||||
const [quotas, setQuotas] = useState<QuotaType[]>([]);
|
||||
const [userPlan, setUserPlan] = useState<UserPlan>('free');
|
||||
|
||||
useEffect(() => {
|
||||
if (!user || !token) return;
|
||||
|
||||
const userPlan = getUserPlan(token);
|
||||
const storagPlan = getStoragePlanData(token);
|
||||
const storageQuota: QuotaType = {
|
||||
name: _('Storage'),
|
||||
tooltip: _('{{percentage}}% of Cloud Storage Used.', {
|
||||
percentage: Math.round((storagPlan.usage / storagPlan.quota) * 100),
|
||||
}),
|
||||
used: Math.round(storagPlan.usage / 1024 / 1024),
|
||||
total: Math.round(storagPlan.quota / 1024 / 1024),
|
||||
unit: 'MB',
|
||||
};
|
||||
const translationPlan = getTranslationPlanData(token);
|
||||
const translationQuota: QuotaType = {
|
||||
name: _('Translation Characters'),
|
||||
tooltip: _('{{percentage}}% of Daily Translation Characters Used.', {
|
||||
percentage: Math.round((translationPlan.usage / translationPlan.quota) * 100),
|
||||
}),
|
||||
used: Math.round(translationPlan.usage / 1024),
|
||||
total: Math.round(translationPlan.quota / 1024),
|
||||
unit: 'K',
|
||||
};
|
||||
setUserPlan(userPlan);
|
||||
setQuotas([storageQuota, translationQuota]);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [token]);
|
||||
|
||||
return {
|
||||
quotas,
|
||||
userPlan,
|
||||
};
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState, useCallback, useEffect } from 'react';
|
||||
import { useAuth } from '@/context/AuthContext';
|
||||
import { getTranslator, getTranslators } from '@/services/translators';
|
||||
import { getTranslator, getTranslators, TranslatorName } from '@/services/translators';
|
||||
import { getFromCache, storeInCache, polish, UseTranslatorOptions } from '@/services/translators';
|
||||
|
||||
export function useTranslator({
|
||||
@@ -11,6 +11,7 @@ export function useTranslator({
|
||||
}: UseTranslatorOptions = {}) {
|
||||
const { token } = useAuth();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [selectedProvider, setSelectedProvider] = useState(provider);
|
||||
const [translator, setTransltor] = useState(() => getTranslator(provider));
|
||||
const [translators] = useState(() => getTranslators());
|
||||
|
||||
@@ -19,7 +20,15 @@ export function useTranslator({
|
||||
}, [provider, sourceLang, targetLang]);
|
||||
|
||||
useEffect(() => {
|
||||
setTransltor(getTranslator(provider));
|
||||
const availableTranslators = getTranslators().filter(
|
||||
(t) => (t.authRequired ? !!token : true) && !t.quotaExceeded,
|
||||
);
|
||||
const selectedTranslator =
|
||||
availableTranslators.find((t) => t.name === provider) || availableTranslators[0]!;
|
||||
const selectedProviderName = selectedTranslator.name as TranslatorName;
|
||||
setTransltor(getTranslator(selectedProviderName));
|
||||
setSelectedProvider(selectedProviderName);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [provider]);
|
||||
|
||||
const translate = useCallback(
|
||||
@@ -47,7 +56,7 @@ export function useTranslator({
|
||||
text,
|
||||
sourceLanguage,
|
||||
targetLanguage,
|
||||
provider,
|
||||
selectedProvider,
|
||||
);
|
||||
if (cachedTranslation) return;
|
||||
|
||||
@@ -59,7 +68,7 @@ export function useTranslator({
|
||||
if (textsNeedingTranslation.length === 0) {
|
||||
const results = await Promise.all(
|
||||
textsToTranslate.map((text) =>
|
||||
getFromCache(text, sourceLanguage, targetLanguage, provider).then(
|
||||
getFromCache(text, sourceLanguage, targetLanguage, selectedProvider).then(
|
||||
(cached) => cached || text,
|
||||
),
|
||||
),
|
||||
@@ -71,9 +80,9 @@ export function useTranslator({
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
const translator = translators.find((t) => t.name === provider);
|
||||
const translator = translators.find((t) => t.name === selectedProvider);
|
||||
if (!translator) {
|
||||
throw new Error(`No translator found for provider: ${provider}`);
|
||||
throw new Error(`No translator found for provider: ${selectedProvider}`);
|
||||
}
|
||||
const translatedTexts = await translator.translate(
|
||||
textsNeedingTranslation,
|
||||
@@ -90,7 +99,7 @@ export function useTranslator({
|
||||
translatedTexts[index] || '',
|
||||
sourceLanguage,
|
||||
targetLanguage,
|
||||
provider,
|
||||
selectedProvider,
|
||||
);
|
||||
}),
|
||||
);
|
||||
@@ -110,7 +119,7 @@ export function useTranslator({
|
||||
originalText,
|
||||
sourceLanguage,
|
||||
targetLanguage,
|
||||
provider,
|
||||
selectedProvider,
|
||||
);
|
||||
|
||||
if (cachedTranslation) {
|
||||
@@ -129,7 +138,7 @@ export function useTranslator({
|
||||
}
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[provider, sourceLang, targetLang, translator, token],
|
||||
[selectedProvider, sourceLang, targetLang, translator, token],
|
||||
);
|
||||
|
||||
return {
|
||||
|
||||
@@ -4,17 +4,11 @@ import { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { corsAllMethods, runMiddleware } from '@/utils/cors';
|
||||
import { getCloudflareContext } from '@opennextjs/cloudflare';
|
||||
import { getDailyTranslationPlanData, getUserPlan } from '@/utils/access';
|
||||
import { ErrorCodes } from '@/services/translators';
|
||||
|
||||
const DEFAULT_DEEPL_FREE_API = 'https://api-free.deepl.com/v2/translate';
|
||||
const DEFAULT_DEEPL_PRO_API = 'https://api.deepl.com/v2/translate';
|
||||
|
||||
const ErrorCodes = {
|
||||
UNAUTHORIZED: 'Unauthorized',
|
||||
DEEPL_API_ERROR: 'DeepL API Error',
|
||||
DAILY_QUOTA_EXCEEDED: 'Daily Quota Exceeded',
|
||||
INTERNAL_SERVER_ERROR: 'Internal Server Error',
|
||||
};
|
||||
|
||||
interface KVNamespace {
|
||||
get(key: string): Promise<string | null>;
|
||||
put(key: string, value: string, options?: { expirationTtl?: number }): Promise<void>;
|
||||
@@ -109,7 +103,7 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
}
|
||||
}
|
||||
|
||||
// if (!user || !token) return res.status(401).json({ error: ErrorCodes.UNAUTHORIZED });
|
||||
if (!user || !token) return res.status(401).json({ error: ErrorCodes.UNAUTHORIZED });
|
||||
|
||||
return await callDeepLAPI(
|
||||
user?.id,
|
||||
|
||||
@@ -536,9 +536,9 @@ export const DEFAULT_STORAGE_QUOTA: UserStorageQuota = {
|
||||
};
|
||||
|
||||
export const DEFAULT_DAILY_TRANSLATION_QUOTA: UserDailyTranslationQuota = {
|
||||
free: 100 * 1024,
|
||||
plus: 1 * 1024 * 1024,
|
||||
pro: 10 * 1024 * 1024,
|
||||
free: 50 * 1024,
|
||||
plus: 500 * 1024,
|
||||
pro: 1024 * 1024,
|
||||
};
|
||||
|
||||
export const DOUBLE_CLICK_INTERVAL_THRESHOLD_MS = 250;
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
import { getAPIBaseUrl } from '@/services/environment';
|
||||
import { stubTranslation as _ } from '@/utils/misc';
|
||||
import { TranslationProvider } from '../types';
|
||||
import { ErrorCodes, TranslationProvider } from '../types';
|
||||
import { UserPlan } from '@/types/user';
|
||||
import { getUserPlan } from '@/utils/access';
|
||||
import { DEFAULT_DAILY_TRANSLATION_QUOTA } from '@/services/constants';
|
||||
import { saveDailyUsage } from '../utils';
|
||||
|
||||
const DEEPL_API_ENDPOINT = getAPIBaseUrl() + '/deepl/translate';
|
||||
|
||||
export const deeplProvider: TranslationProvider = {
|
||||
name: 'deepl',
|
||||
label: _('DeepL'),
|
||||
authRequired: true,
|
||||
quotaExceeded: false,
|
||||
translate: async (
|
||||
text: string[],
|
||||
sourceLang: string,
|
||||
@@ -14,14 +20,22 @@ export const deeplProvider: TranslationProvider = {
|
||||
token?: string | null,
|
||||
useCache: boolean = false,
|
||||
): Promise<string[]> => {
|
||||
const authRequired = deeplProvider.authRequired;
|
||||
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
|
||||
let userPlan: UserPlan = 'free';
|
||||
if (token) {
|
||||
userPlan = getUserPlan(token);
|
||||
headers['Authorization'] = `Bearer ${token}`;
|
||||
}
|
||||
|
||||
if (authRequired && !token) {
|
||||
throw new Error('Authentication token is required for DeepL translation');
|
||||
}
|
||||
|
||||
const body = JSON.stringify({
|
||||
text: text,
|
||||
source_lang: sourceLang.toUpperCase(),
|
||||
@@ -29,22 +43,36 @@ export const deeplProvider: TranslationProvider = {
|
||||
use_cache: useCache,
|
||||
});
|
||||
|
||||
const response = await fetch(DEEPL_API_ENDPOINT, { method: 'POST', headers, body });
|
||||
const quota = DEFAULT_DAILY_TRANSLATION_QUOTA[userPlan];
|
||||
try {
|
||||
const response = await fetch(DEEPL_API_ENDPOINT, { method: 'POST', headers, body });
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Translation failed with status ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
if (!data || !data.translations) {
|
||||
throw new Error('Invalid response from translation service');
|
||||
}
|
||||
|
||||
return text.map((line, i) => {
|
||||
if (!line?.trim().length) {
|
||||
return line;
|
||||
if (!response.ok) {
|
||||
throw new Error(`Translation failed with status ${response.status}`);
|
||||
}
|
||||
return data.translations?.[i]?.text || line;
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
if (!data || !data.translations) {
|
||||
throw new Error('Invalid response from translation service');
|
||||
}
|
||||
|
||||
return text.map((line, i) => {
|
||||
if (!line?.trim().length) {
|
||||
return line;
|
||||
}
|
||||
const translation = data.translations?.[i];
|
||||
if (translation?.daily_usage) {
|
||||
saveDailyUsage(translation.daily_usage);
|
||||
deeplProvider.quotaExceeded = data.daily_usage >= quota;
|
||||
}
|
||||
return translation?.text || line;
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof Error && error.message.includes(ErrorCodes.DAILY_QUOTA_EXCEEDED)) {
|
||||
saveDailyUsage(quota);
|
||||
deeplProvider.quotaExceeded = true;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
@@ -3,6 +3,8 @@ import { TranslatorName } from './providers';
|
||||
export interface TranslationProvider {
|
||||
name: string;
|
||||
label: string;
|
||||
authRequired?: boolean;
|
||||
quotaExceeded?: boolean;
|
||||
translate: (
|
||||
texts: string[],
|
||||
sourceLang: string,
|
||||
@@ -22,3 +24,10 @@ export interface UseTranslatorOptions {
|
||||
targetLang?: string;
|
||||
enablePolishing?: boolean;
|
||||
}
|
||||
|
||||
export const ErrorCodes = {
|
||||
UNAUTHORIZED: 'Unauthorized',
|
||||
DEEPL_API_ERROR: 'DeepL API Error',
|
||||
DAILY_QUOTA_EXCEEDED: 'Daily Quota Exceeded',
|
||||
INTERNAL_SERVER_ERROR: 'Internal Server Error',
|
||||
};
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
const DAILY_USAGE_KEY = 'translationDailyUsage';
|
||||
|
||||
export const saveDailyUsage = (usage: number, date?: string) => {
|
||||
if (typeof window !== 'undefined') {
|
||||
const isoDate = date || new Date().toISOString().split('T')[0]!;
|
||||
const dailyUsage = { [isoDate]: usage };
|
||||
localStorage.setItem(DAILY_USAGE_KEY, JSON.stringify(dailyUsage));
|
||||
}
|
||||
};
|
||||
|
||||
export const getDailyUsage = (date?: string): number | null => {
|
||||
if (typeof window !== 'undefined') {
|
||||
const isoDate = date || new Date().toISOString().split('T')[0]!;
|
||||
const usage = localStorage.getItem(DAILY_USAGE_KEY);
|
||||
if (usage) {
|
||||
const dailyUsage = JSON.parse(usage);
|
||||
if (dailyUsage[isoDate]) {
|
||||
return dailyUsage[isoDate];
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
@@ -1,8 +1,9 @@
|
||||
import { jwtDecode } from 'jwt-decode';
|
||||
import { supabase } from '@/utils/supabase';
|
||||
import { UserPlan } from '@/types/user';
|
||||
import { DEFAULT_DAILY_TRANSLATION_QUOTA, DEFAULT_STORAGE_QUOTA } from '@/services/constants';
|
||||
import { isWebAppPlatform } from '@/services/environment';
|
||||
import { supabase } from '@/utils/supabase';
|
||||
import { getDailyUsage } from '@/services/translators/utils';
|
||||
|
||||
interface Token {
|
||||
plan: UserPlan;
|
||||
@@ -29,6 +30,19 @@ export const getStoragePlanData = (token: string) => {
|
||||
};
|
||||
};
|
||||
|
||||
export const getTranslationPlanData = (token: string) => {
|
||||
const data = jwtDecode<Token>(token) || {};
|
||||
const plan: UserPlan = data['plan'] || 'free';
|
||||
const usage = getDailyUsage() || 0;
|
||||
const quota = DEFAULT_DAILY_TRANSLATION_QUOTA[plan];
|
||||
|
||||
return {
|
||||
plan,
|
||||
usage,
|
||||
quota,
|
||||
};
|
||||
};
|
||||
|
||||
export const getDailyTranslationPlanData = (token: string) => {
|
||||
const data = jwtDecode<Token>(token) || {};
|
||||
const plan = data['plan'] || 'free';
|
||||
|
||||
Reference in New Issue
Block a user