translator: add daily translation quota for deepl (#1275)
This commit is contained in:
@@ -98,7 +98,7 @@ const TranslatorPopup: React.FC<TranslatorPopupProps> = ({
|
||||
setTranslation(null);
|
||||
|
||||
try {
|
||||
const input = text.replaceAll('\n', ' ').trim();
|
||||
const input = text.replaceAll('\n', '').trim();
|
||||
const result = await translate([input]);
|
||||
const translatedText = result[0];
|
||||
const detectedSource = null;
|
||||
|
||||
@@ -65,7 +65,7 @@ export function useTranslator({
|
||||
),
|
||||
);
|
||||
|
||||
return results;
|
||||
return enablePolishing ? polish(results, targetLanguage) : results;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
|
||||
@@ -2,11 +2,18 @@ import crypto from 'crypto';
|
||||
import { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { corsAllMethods, runMiddleware } from '@/utils/cors';
|
||||
import { supabase } from '@/utils/supabase';
|
||||
import { getUserPlan } from '@/utils/access';
|
||||
import { getDailyTranslationPlanData, getUserPlan } from '@/utils/access';
|
||||
|
||||
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>;
|
||||
@@ -101,7 +108,11 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
}
|
||||
}
|
||||
|
||||
// if (!user || !token) return res.status(401).json({ error: ErrorCodes.UNAUTHORIZED });
|
||||
|
||||
return await callDeepLAPI(
|
||||
user?.id,
|
||||
token,
|
||||
singleText,
|
||||
sourceLang,
|
||||
targetLang,
|
||||
@@ -115,11 +126,16 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
return res.status(200).json({ translations });
|
||||
} catch (error) {
|
||||
console.error('Error proxying DeepL request:', error);
|
||||
return res.status(500).json({ error: 'Internal Server Error' });
|
||||
if (error instanceof Error && error.message.includes(ErrorCodes.DAILY_QUOTA_EXCEEDED)) {
|
||||
return res.status(429).json({ error: ErrorCodes.DAILY_QUOTA_EXCEEDED });
|
||||
}
|
||||
return res.status(500).json({ error: ErrorCodes.INTERNAL_SERVER_ERROR });
|
||||
}
|
||||
};
|
||||
|
||||
async function callDeepLAPI(
|
||||
userId: string | undefined,
|
||||
token: string | undefined,
|
||||
text: string,
|
||||
sourceLang: string,
|
||||
targetLang: string,
|
||||
@@ -128,11 +144,22 @@ async function callDeepLAPI(
|
||||
translationsKV: KVNamespace | undefined,
|
||||
useCache: boolean,
|
||||
) {
|
||||
let dailyUsageKey = '';
|
||||
if (userId && token) {
|
||||
const { quota: dailyQuota } = getDailyTranslationPlanData(token);
|
||||
const currentDate = new Date().toISOString().split('T')[0]!;
|
||||
dailyUsageKey = `daily_usage:${currentDate}:${userId}`;
|
||||
const dailyUsage = (await translationsKV?.get(dailyUsageKey)) || '0';
|
||||
if (dailyQuota <= parseInt(dailyUsage) + text.length) {
|
||||
throw new Error(ErrorCodes.DAILY_QUOTA_EXCEEDED);
|
||||
}
|
||||
}
|
||||
|
||||
const isV2Api = apiUrl.endsWith('/v2/translate');
|
||||
|
||||
// TODO: this should be processed in the client, but for now, we need to do it here
|
||||
// please remove this when most clients are updated
|
||||
const input = text.replaceAll('\n', ' ').trim();
|
||||
const input = text.replaceAll('\n', '').trim();
|
||||
const requestBody = {
|
||||
text: isV2Api ? [input] : input,
|
||||
source_lang: isV2Api ? sourceLang : (LANG_V2_V1_MAP[sourceLang] ?? sourceLang),
|
||||
@@ -166,6 +193,20 @@ async function callDeepLAPI(
|
||||
translatedText = data.data;
|
||||
}
|
||||
|
||||
let newDailyUsage = 0;
|
||||
if (dailyUsageKey && translationsKV) {
|
||||
try {
|
||||
const usage = translatedText.length + text.length;
|
||||
const dailyUsage = (await translationsKV.get(dailyUsageKey)) || '0';
|
||||
newDailyUsage = parseInt(dailyUsage) + usage;
|
||||
await translationsKV.put(dailyUsageKey, newDailyUsage.toString(), {
|
||||
expirationTtl: 86400 * 30,
|
||||
});
|
||||
} catch (cacheError) {
|
||||
console.error('Cache storage error:', cacheError);
|
||||
}
|
||||
}
|
||||
|
||||
if (useCache && translationsKV && translatedText) {
|
||||
try {
|
||||
const cacheKey = generateCacheKey(text, sourceLang, targetLang);
|
||||
@@ -177,6 +218,7 @@ async function callDeepLAPI(
|
||||
|
||||
return {
|
||||
text: translatedText,
|
||||
daily_usage: newDailyUsage,
|
||||
detected_source_language: detectedSourceLanguage,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
ViewSettings,
|
||||
} from '@/types/book';
|
||||
import { ReadSettings, SystemSettings } from '@/types/settings';
|
||||
import { UserStorageQuota } from '@/types/user';
|
||||
import { UserStorageQuota, UserDailyTranslationQuota } from '@/types/user';
|
||||
import { getDefaultMaxBlockSize, getDefaultMaxInlineSize } from '@/utils/config';
|
||||
import { stubTranslation as _ } from '@/utils/misc';
|
||||
|
||||
@@ -527,6 +527,12 @@ export const DEFAULT_STORAGE_QUOTA: UserStorageQuota = {
|
||||
pro: 10 * 1024 * 1024 * 1024,
|
||||
};
|
||||
|
||||
export const DEFAULT_DAILY_TRANSLATION_QUOTA: UserDailyTranslationQuota = {
|
||||
free: 100 * 1024,
|
||||
plus: 1 * 1024 * 1024,
|
||||
pro: 10 * 1024 * 1024,
|
||||
};
|
||||
|
||||
export const DOUBLE_CLICK_INTERVAL_THRESHOLD_MS = 250;
|
||||
export const DISABLE_DOUBLE_CLICK_ON_MOBILE = true;
|
||||
export const LONG_HOLD_THRESHOLD = 500;
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
export type UserStorageQuota = {
|
||||
export interface UserQuota {
|
||||
free: number;
|
||||
plus: number;
|
||||
pro: number;
|
||||
};
|
||||
}
|
||||
|
||||
export type UserPlan = keyof UserStorageQuota;
|
||||
export type UserPlan = keyof UserQuota;
|
||||
export type UserStorageQuota = UserQuota;
|
||||
export type UserDailyTranslationQuota = UserQuota;
|
||||
|
||||
export type QuotaType = {
|
||||
name: string;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { jwtDecode } from 'jwt-decode';
|
||||
import { UserPlan } from '@/types/user';
|
||||
import { DEFAULT_STORAGE_QUOTA } from '@/services/constants';
|
||||
import { DEFAULT_DAILY_TRANSLATION_QUOTA, DEFAULT_STORAGE_QUOTA } from '@/services/constants';
|
||||
import { isWebAppPlatform } from '@/services/environment';
|
||||
import { supabase } from '@/utils/supabase';
|
||||
|
||||
@@ -29,6 +29,19 @@ export const getStoragePlanData = (token: string) => {
|
||||
};
|
||||
};
|
||||
|
||||
export const getDailyTranslationPlanData = (token: string) => {
|
||||
const data = jwtDecode<Token>(token) || {};
|
||||
const plan = data['plan'] || 'free';
|
||||
const fixedQuota = parseInt(process.env['NEXT_PUBLIC_TRANSLATION_FIXED_QUOTA'] || '0');
|
||||
const quota =
|
||||
fixedQuota || DEFAULT_DAILY_TRANSLATION_QUOTA[plan] || DEFAULT_DAILY_TRANSLATION_QUOTA['free'];
|
||||
|
||||
return {
|
||||
plan,
|
||||
quota,
|
||||
};
|
||||
};
|
||||
|
||||
export const getAccessToken = async (): Promise<string | null> => {
|
||||
// In browser context there might be two instances of supabase one in the app route
|
||||
// and the other in the pages route, and they might have different sessions
|
||||
|
||||
Reference in New Issue
Block a user