From d3093a1c4efeba8fe7032bdc94f70902d846027e Mon Sep 17 00:00:00 2001 From: codytseng Date: Sun, 29 Jun 2025 14:13:18 +0800 Subject: [PATCH] feat: support translation for profile abount --- src/components/Nip05/index.tsx | 2 +- src/components/ProfileAbout/index.tsx | 75 ++++++++++++- src/components/TranslateButton/index.tsx | 76 ++----------- src/i18n/locales/ar.ts | 5 +- src/i18n/locales/de.ts | 5 +- src/i18n/locales/en.ts | 5 +- src/i18n/locales/es.ts | 5 +- src/i18n/locales/fr.ts | 5 +- src/i18n/locales/it.ts | 5 +- src/i18n/locales/ja.ts | 5 +- src/i18n/locales/pl.ts | 5 +- src/i18n/locales/pt-BR.ts | 5 +- src/i18n/locales/pt-PT.ts | 5 +- src/i18n/locales/ru.ts | 5 +- src/i18n/locales/th.ts | 5 +- src/i18n/locales/zh.ts | 5 +- src/lib/utils.ts | 72 ++++++++++++ src/providers/TranslationServiceProvider.tsx | 111 ++++++++++++------- src/services/libre-translate.service.ts | 11 +- src/services/translation.service.ts | 11 +- 20 files changed, 290 insertions(+), 133 deletions(-) diff --git a/src/components/Nip05/index.tsx b/src/components/Nip05/index.tsx index 6d75c116..f3b0f6f7 100644 --- a/src/components/Nip05/index.tsx +++ b/src/components/Nip05/index.tsx @@ -42,7 +42,7 @@ export default function Nip05({ pubkey, append }: { pubkey: string; append?: str )} {nip05Domain} diff --git a/src/components/ProfileAbout/index.tsx b/src/components/ProfileAbout/index.tsx index b505c5b5..1cf9e9ba 100644 --- a/src/components/ProfileAbout/index.tsx +++ b/src/components/ProfileAbout/index.tsx @@ -5,19 +5,33 @@ import { EmbeddedWebsocketUrlParser, parseContent } from '@/lib/content-parser' -import { useMemo } from 'react' +import { detectLanguage } from '@/lib/utils' +import { useMemo, useState } from 'react' +import { useTranslation } from 'react-i18next' import { EmbeddedHashtag, EmbeddedMention, EmbeddedNormalUrl, EmbeddedWebsocketUrl } from '../Embedded' +import { useTranslationService } from '@/providers/TranslationServiceProvider' +import { toast } from 'sonner' export default function ProfileAbout({ about, className }: { about?: string; className?: string }) { + const { t, i18n } = useTranslation() + const { translateText } = useTranslationService() + const needTranslation = useMemo(() => { + const detected = detectLanguage(about) + if (!detected) return false + if (detected === 'und') return true + return !i18n.language.startsWith(detected) + }, [about, i18n.language]) + const [translatedAbout, setTranslatedAbout] = useState(null) + const [translating, setTranslating] = useState(false) const aboutNodes = useMemo(() => { if (!about) return null - const nodes = parseContent(about, [ + const nodes = parseContent(translatedAbout ?? about, [ EmbeddedWebsocketUrlParser, EmbeddedNormalUrlParser, EmbeddedHashtagParser, @@ -40,7 +54,60 @@ export default function ProfileAbout({ about, className }: { about?: string; cla return } }) - }, [about]) + }, [about, translatedAbout]) - return
{aboutNodes}
+ const handleTranslate = async () => { + if (translating || translatedAbout) return + setTranslating(true) + translateText(about ?? '') + .then((translated) => { + setTranslatedAbout(translated) + }) + .catch((error) => { + toast.error( + 'Translation failed: ' + + (error.message || 'An error occurred while translating the about') + ) + }) + .finally(() => { + setTranslating(false) + }) + } + + const handleShowOriginal = () => { + setTranslatedAbout(null) + } + + return ( +
+
{aboutNodes}
+ {needTranslation && ( +
+ {translating ? ( +
{t('Translating...')}
+ ) : translatedAbout === null ? ( + + ) : ( + + )} +
+ )} +
+ ) } diff --git a/src/components/TranslateButton/index.tsx b/src/components/TranslateButton/index.tsx index b116507b..04dcd72d 100644 --- a/src/components/TranslateButton/index.tsx +++ b/src/components/TranslateButton/index.tsx @@ -1,19 +1,9 @@ -import { - EMAIL_REGEX, - EMBEDDED_EVENT_REGEX, - EMBEDDED_MENTION_REGEX, - EMOJI_REGEX, - HASHTAG_REGEX, - URL_REGEX, - WS_URL_REGEX -} from '@/constants' import { useTranslatedEvent } from '@/hooks' import { isSupportedKind } from '@/lib/event' import { toTranslation } from '@/lib/link' -import { cn } from '@/lib/utils' +import { cn, detectLanguage } from '@/lib/utils' import { useSecondaryPage } from '@/PageManager' import { useTranslationService } from '@/providers/TranslationServiceProvider' -import { franc } from 'franc-min' import { Languages, Loader } from 'lucide-react' import { Event } from 'nostr-tools' import { useMemo, useState } from 'react' @@ -29,68 +19,16 @@ export default function TranslateButton({ }) { const { i18n } = useTranslation() const { push } = useSecondaryPage() - const { translate, showOriginalEvent } = useTranslationService() + const { translateEvent, showOriginalEvent } = useTranslationService() const [translating, setTranslating] = useState(false) const translatedEvent = useTranslatedEvent(event.id) const supported = useMemo(() => isSupportedKind(event.kind), [event]) const needTranslation = useMemo(() => { - const cleanText = event.content - .replace(URL_REGEX, '') - .replace(WS_URL_REGEX, '') - .replace(EMAIL_REGEX, '') - .replace(EMBEDDED_MENTION_REGEX, '') - .replace(EMBEDDED_EVENT_REGEX, '') - .replace(HASHTAG_REGEX, '') - .replace(EMOJI_REGEX, '') - .trim() - - if (!cleanText) { - return false - } - - if (/[\u3040-\u309f\u30a0-\u30ff]/.test(cleanText)) { - return i18n.language !== 'ja' - } - if (/[\u0e00-\u0e7f]/.test(cleanText)) { - return i18n.language !== 'th' - } - if (/[\u4e00-\u9fff]/.test(cleanText)) { - return i18n.language !== 'zh' - } - if (/[\u0600-\u06ff]/.test(cleanText)) { - return i18n.language !== 'ar' - } - if (/[\u0400-\u04ff]/.test(cleanText)) { - return i18n.language !== 'ru' - } - - try { - const detectedLang = franc(cleanText) - const langMap: { [key: string]: string } = { - ara: 'ar', // Arabic - deu: 'de', // German - eng: 'en', // English - spa: 'es', // Spanish - fra: 'fr', // French - ita: 'it', // Italian - jpn: 'ja', // Japanese - pol: 'pl', // Polish - por: 'pt', // Portuguese - rus: 'ru', // Russian - cmn: 'zh', // Chinese (Mandarin) - zho: 'zh' // Chinese (alternative code) - } - - const normalizedLang = langMap[detectedLang] - if (!normalizedLang) { - return true - } - - return !i18n.language.startsWith(normalizedLang) - } catch { - return true - } + const detected = detectLanguage(event.content) + if (!detected) return false + if (detected === 'und') return true + return !i18n.language.startsWith(detected) }, [event, i18n.language]) if (!supported || !needTranslation) { @@ -101,7 +39,7 @@ export default function TranslateButton({ if (translating) return setTranslating(true) - await translate(event) + await translateEvent(event) .catch((error) => { toast.error( 'Translation failed: ' + (error.message || 'An error occurred while translating the note') diff --git a/src/i18n/locales/ar.ts b/src/i18n/locales/ar.ts index d3e75a30..3d412b6c 100644 --- a/src/i18n/locales/ar.ts +++ b/src/i18n/locales/ar.ts @@ -275,6 +275,9 @@ export default { 'المستخدمون الموثوقون هم الأشخاص الذين تتابعهم والأشخاص الذين يتابعونهم.', Continue: 'متابعة', 'Successfully updated mute list': 'تم تحديث قائمة الكتم بنجاح', - 'No pubkeys found from {url}': 'لم يتم العثور على مفاتيح عامة من {{url}}' + 'No pubkeys found from {url}': 'لم يتم العثور على مفاتيح عامة من {{url}}', + 'Translating...': 'جارٍ الترجمة...', + Translate: 'ترجمة', + 'Show original': 'عرض الأصل' } } diff --git a/src/i18n/locales/de.ts b/src/i18n/locales/de.ts index 1b995801..f0b49545 100644 --- a/src/i18n/locales/de.ts +++ b/src/i18n/locales/de.ts @@ -282,6 +282,9 @@ export default { 'Vertrauenswürdige Benutzer sind Personen, denen du folgst, und Personen, denen sie folgen.', Continue: 'Weiter', 'Successfully updated mute list': 'Stummschalteliste erfolgreich aktualisiert', - 'No pubkeys found from {url}': 'Keine Pubkeys von {{url}} gefunden' + 'No pubkeys found from {url}': 'Keine Pubkeys von {{url}} gefunden', + 'Translating...': 'Übersetze...', + Translate: 'Übersetzen', + 'Show original': 'Original anzeigen' } } diff --git a/src/i18n/locales/en.ts b/src/i18n/locales/en.ts index ac47e229..fb5b3174 100644 --- a/src/i18n/locales/en.ts +++ b/src/i18n/locales/en.ts @@ -275,6 +275,9 @@ export default { 'Trusted users include people you follow and people they follow.', Continue: 'Continue', 'Successfully updated mute list': 'Successfully updated mute list', - 'No pubkeys found from {url}': 'No pubkeys found from {{url}}' + 'No pubkeys found from {url}': 'No pubkeys found from {{url}}', + 'Translating...': 'Translating...', + Translate: 'Translate', + 'Show original': 'Show original' } } diff --git a/src/i18n/locales/es.ts b/src/i18n/locales/es.ts index 13c42fdb..b1818c20 100644 --- a/src/i18n/locales/es.ts +++ b/src/i18n/locales/es.ts @@ -280,6 +280,9 @@ export default { 'Los usuarios confiables incluyen a las personas que sigues y a las personas que ellos siguen.', Continue: 'Continuar', 'Successfully updated mute list': 'Lista de silenciamiento actualizada con éxito', - 'No pubkeys found from {url}': 'No se encontraron pubkeys desde {{url}}' + 'No pubkeys found from {url}': 'No se encontraron pubkeys desde {{url}}', + 'Translating...': 'Traduciendo...', + Translate: 'Traducir', + 'Show original': 'Mostrar original' } } diff --git a/src/i18n/locales/fr.ts b/src/i18n/locales/fr.ts index 8515b405..55fb2810 100644 --- a/src/i18n/locales/fr.ts +++ b/src/i18n/locales/fr.ts @@ -280,6 +280,9 @@ export default { 'Les utilisateurs de confiance incluent les personnes que vous suivez et les personnes qu’elles suivent.', Continue: 'Continuer', 'Successfully updated mute list': 'Liste de sourdine mise à jour avec succès', - 'No pubkeys found from {url}': 'Aucun pubkey trouvé à partir de {{url}}' + 'No pubkeys found from {url}': 'Aucun pubkey trouvé à partir de {{url}}', + 'Translating...': 'Traduction en cours...', + Translate: 'Traduire', + 'Show original': 'Afficher l’original' } } diff --git a/src/i18n/locales/it.ts b/src/i18n/locales/it.ts index b709283f..7d4859e0 100644 --- a/src/i18n/locales/it.ts +++ b/src/i18n/locales/it.ts @@ -279,6 +279,9 @@ export default { 'Gli utenti fidati includono le persone che segui e le persone che seguono loro.', Continue: 'Continua', 'Successfully updated mute list': 'Lista di silenziamento aggiornata con successo', - 'No pubkeys found from {url}': 'Nessun pubkey trovato da {{url}}' + 'No pubkeys found from {url}': 'Nessun pubkey trovato da {{url}}', + 'Translating...': 'Traduzione in corso...', + Translate: 'Traduci', + 'Show original': 'Mostra originale' } } diff --git a/src/i18n/locales/ja.ts b/src/i18n/locales/ja.ts index e824f916..63daeec6 100644 --- a/src/i18n/locales/ja.ts +++ b/src/i18n/locales/ja.ts @@ -277,6 +277,9 @@ export default { '信頼できるユーザーには、あなたがフォローしている人とその人がフォローしている人が含まれます。', Continue: '続行', 'Successfully updated mute list': 'ミュートリストの更新に成功しました', - 'No pubkeys found from {url}': 'URL {{url}} からのpubkeyは見つかりませんでした' + 'No pubkeys found from {url}': 'URL {{url}} からのpubkeyは見つかりませんでした', + 'Translating...': '翻訳中...', + Translate: '翻訳', + 'Show original': '原文を表示' } } diff --git a/src/i18n/locales/pl.ts b/src/i18n/locales/pl.ts index 103f911f..65160650 100644 --- a/src/i18n/locales/pl.ts +++ b/src/i18n/locales/pl.ts @@ -278,6 +278,9 @@ export default { 'Zaufani użytkownicy to osoby, które obserwujesz i osoby, które oni obserwują.', Continue: 'Kontynuuj', 'Successfully updated mute list': 'Sukces aktualizacji listy zablokowanych użytkowników', - 'No pubkeys found from {url}': 'Nie znaleziono kluczy publicznych z {{url}}' + 'No pubkeys found from {url}': 'Nie znaleziono kluczy publicznych z {{url}}', + 'Translating...': 'Tłumaczenie...', + Translate: 'Przetłumacz', + 'Show original': 'Pokaż oryginał' } } diff --git a/src/i18n/locales/pt-BR.ts b/src/i18n/locales/pt-BR.ts index ce2a6068..9b94b729 100644 --- a/src/i18n/locales/pt-BR.ts +++ b/src/i18n/locales/pt-BR.ts @@ -278,6 +278,9 @@ export default { 'Usuários confiáveis incluem pessoas que você segue e pessoas que elas seguem.', Continue: 'Continuar', 'Successfully updated mute list': 'Lista de silenciados atualizada com sucesso', - 'No pubkeys found from {url}': 'Nenhum pubkey encontrado em {{url}}' + 'No pubkeys found from {url}': 'Nenhum pubkey encontrado em {{url}}', + 'Translating...': 'Traduzindo...', + Translate: 'Traduzir', + 'Show original': 'Mostrar original' } } diff --git a/src/i18n/locales/pt-PT.ts b/src/i18n/locales/pt-PT.ts index a3246648..aad496f9 100644 --- a/src/i18n/locales/pt-PT.ts +++ b/src/i18n/locales/pt-PT.ts @@ -279,6 +279,9 @@ export default { 'Usuários confiáveis incluem pessoas que você segue e pessoas que elas seguem.', Continue: 'Continuar', 'Successfully updated mute list': 'Lista de silenciados atualizada com sucesso', - 'No pubkeys found from {url}': 'Nenhum pubkey encontrado em {{url}}' + 'No pubkeys found from {url}': 'Nenhum pubkey encontrado em {{url}}', + 'Translating...': 'Traduzindo...', + Translate: 'Traduzir', + 'Show original': 'Mostrar original' } } diff --git a/src/i18n/locales/ru.ts b/src/i18n/locales/ru.ts index f8bb4420..5b1cb5cf 100644 --- a/src/i18n/locales/ru.ts +++ b/src/i18n/locales/ru.ts @@ -280,6 +280,9 @@ export default { 'Доверенные пользователи включают людей, на которых вы подписаны, и людей, на которых они подписаны.', Continue: 'Продолжить', 'Successfully updated mute list': 'Успешно обновлен список заглушенных пользователей', - 'No pubkeys found from {url}': 'Не найдено pubkeys из {{url}}' + 'No pubkeys found from {url}': 'Не найдено pubkeys из {{url}}', + 'Translating...': 'Перевод...', + Translate: 'Перевести', + 'Show original': 'Показать оригинал' } } diff --git a/src/i18n/locales/th.ts b/src/i18n/locales/th.ts index daa46522..8ec7dd7d 100644 --- a/src/i18n/locales/th.ts +++ b/src/i18n/locales/th.ts @@ -274,6 +274,9 @@ export default { 'ผู้ใช้ที่เชื่อถือได้รวมถึงผู้ที่คุณติดตามและผู้ที่พวกเขาติดตาม', Continue: 'ดำเนินการต่อ', 'Successfully updated mute list': 'อัปเดตรายการปิดเสียงสำเร็จ', - 'No pubkeys found from {url}': 'ไม่พบ pubkeys จาก {{url}}' + 'No pubkeys found from {url}': 'ไม่พบ pubkeys จาก {{url}}', + 'Translating...': 'กำลังแปล...', + Translate: 'แปล', + 'Show original': 'แสดงต้นฉบับ' } } diff --git a/src/i18n/locales/zh.ts b/src/i18n/locales/zh.ts index 49043237..1b023e33 100644 --- a/src/i18n/locales/zh.ts +++ b/src/i18n/locales/zh.ts @@ -275,6 +275,9 @@ export default { '受信任的用户包括您关注的人和他们关注的人。', Continue: '继续', 'Successfully updated mute list': '成功更新屏蔽列表', - 'No pubkeys found from {url}': '在 {{url}} 中未找到 pubkeys' + 'No pubkeys found from {url}': '在 {{url}} 中未找到 pubkeys', + 'Translating...': '翻译中...', + Translate: '翻译', + 'Show original': '显示原文' } } diff --git a/src/lib/utils.ts b/src/lib/utils.ts index a91496e8..b1670d40 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -1,4 +1,14 @@ +import { + EMAIL_REGEX, + EMBEDDED_EVENT_REGEX, + EMBEDDED_MENTION_REGEX, + EMOJI_REGEX, + HASHTAG_REGEX, + URL_REGEX, + WS_URL_REGEX +} from '@/constants' import { clsx, type ClassValue } from 'clsx' +import { franc } from 'franc-min' import { twMerge } from 'tailwind-merge' export function cn(...inputs: ClassValue[]) { @@ -38,3 +48,65 @@ export function isInViewport(el: HTMLElement) { rect.right <= (window.innerWidth || document.documentElement.clientWidth) ) } + +export function detectLanguage(text?: string): string | null { + if (!text) { + return null + } + const cleanText = text + .replace(URL_REGEX, '') + .replace(WS_URL_REGEX, '') + .replace(EMAIL_REGEX, '') + .replace(EMBEDDED_MENTION_REGEX, '') + .replace(EMBEDDED_EVENT_REGEX, '') + .replace(HASHTAG_REGEX, '') + .replace(EMOJI_REGEX, '') + .trim() + + if (!cleanText) { + return null + } + + if (/[\u3040-\u309f\u30a0-\u30ff]/.test(cleanText)) { + return 'ja' + } + if (/[\u0e00-\u0e7f]/.test(cleanText)) { + return 'th' + } + if (/[\u4e00-\u9fff]/.test(cleanText)) { + return 'zh' + } + if (/[\u0600-\u06ff]/.test(cleanText)) { + return 'ar' + } + if (/[\u0400-\u04ff]/.test(cleanText)) { + return 'ru' + } + + try { + const detectedLang = franc(cleanText) + const langMap: { [key: string]: string } = { + ara: 'ar', // Arabic + deu: 'de', // German + eng: 'en', // English + spa: 'es', // Spanish + fra: 'fr', // French + ita: 'it', // Italian + jpn: 'ja', // Japanese + pol: 'pl', // Polish + por: 'pt', // Portuguese + rus: 'ru', // Russian + cmn: 'zh', // Chinese (Mandarin) + zho: 'zh' // Chinese (alternative code) + } + + const normalizedLang = langMap[detectedLang] + if (!normalizedLang) { + return 'und' + } + + return normalizedLang + } catch { + return 'und' + } +} diff --git a/src/providers/TranslationServiceProvider.tsx b/src/providers/TranslationServiceProvider.tsx index 4b4dfa54..b716d894 100644 --- a/src/providers/TranslationServiceProvider.tsx +++ b/src/providers/TranslationServiceProvider.tsx @@ -7,12 +7,14 @@ import { createContext, useContext, useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' import { useNostr } from './NostrProvider' -const translatedEventCache: Record = {} +const translatedEventCache: Map = new Map() +const translatedTextCache: Map = new Map() type TTranslationServiceContext = { config: TTranslationServiceConfig translatedEventIdSet: Set - translate: (event: Event) => Promise + translateText: (text: string) => Promise + translateEvent: (event: Event) => Promise getTranslatedEvent: (eventId: string) => Event | null showOriginalEvent: (eventId: string) => void getAccount: () => Promise @@ -62,61 +64,85 @@ export function TranslationServiceProvider({ children }: { children: React.React const getTranslatedEvent = (eventId: string): Event | null => { const target = i18n.language - const cacheKey = eventId + '_' + target - return translatedEventCache[cacheKey] ?? null + const cacheKey = target + '_' + eventId + return translatedEventCache.get(cacheKey) ?? null } - const translate = async (event: Event): Promise => { + const translate = async (text: string, target: string): Promise => { + if (config.service === 'jumble') { + return await translation.translate(text, target) + } else { + return await libreTranslate.translate(text, target, config.server, config.api_key) + } + } + + const translateText = async (text: string): Promise => { + if (!text) { + return text + } + + const target = i18n.language + const cacheKey = target + '_' + text + const cache = translatedTextCache.get(cacheKey) + if (cache) { + return cache + } + + const translatedText = await translate(text, target) + translatedTextCache.set(cacheKey, translatedText) + return translatedText + } + + const translateHighlightEvent = async (event: Event): Promise => { + const target = i18n.language + const comment = event.tags.find((tag) => tag[0] === 'comment')?.[1] + if (!event.content && !comment) { + return event + } + const [translatedContent, translatedComment] = await Promise.all([ + translate(event.content, target), + !!comment && translate(comment, target) + ]) + + const translatedEvent: Event = { + ...event, + content: translatedContent + } + if (translatedComment) { + translatedEvent.tags = event.tags.map((tag) => + tag[0] === 'comment' ? ['comment', translatedComment] : tag + ) + } + setTranslatedEventIdSet((prev) => new Set(prev.add(event.id))) + return translatedEvent + } + + const translateEvent = async (event: Event): Promise => { if (config.service === 'jumble' && !pubkey) { startLogin() return } const target = i18n.language - const cacheKey = event.id + '_' + target - if (translatedEventCache[cacheKey]) { + const cacheKey = target + '_' + event.id + const cache = translatedEventCache.get(cacheKey) + if (cache) { setTranslatedEventIdSet((prev) => new Set(prev.add(event.id))) - return translatedEventCache[cacheKey] + return cache } + let translatedEvent: Event | undefined if (event.kind === kinds.Highlights) { - const comment = event.tags.find((tag) => tag[0] === 'comment')?.[1] - const [translatedContent, translatedComment] = await Promise.all([ - config.service === 'jumble' - ? await translation.translate(event.content, target) - : await libreTranslate.translate(event.content, target, config.server, config.api_key), - !!comment && - (config.service === 'jumble' - ? await translation.translate(comment, target) - : await libreTranslate.translate(comment, target, config.server, config.api_key)) - ]) - - if (!translatedContent) { + translatedEvent = await translateHighlightEvent(event) + } else { + const translatedText = await translate(event.content, target) + if (!translatedText) { return } - const translatedEvent: Event = { - ...event, - content: translatedContent - } - if (translatedComment) { - translatedEvent.tags = event.tags.map((tag) => - tag[0] === 'comment' ? ['comment', translatedComment] : tag - ) - } - translatedEventCache[cacheKey] = translatedEvent - setTranslatedEventIdSet((prev) => new Set(prev.add(event.id))) - return translatedEvent + translatedEvent = { ...event, content: translatedText } } - const translatedText = - config.service === 'jumble' - ? await translation.translate(event.content, target) - : await libreTranslate.translate(event.content, target, config.server, config.api_key) - if (!translatedText) { - return - } - const translatedEvent: Event = { ...event, content: translatedText } - translatedEventCache[cacheKey] = translatedEvent + translatedEventCache.set(cacheKey, translatedEvent) setTranslatedEventIdSet((prev) => new Set(prev.add(event.id))) return translatedEvent } @@ -141,7 +167,8 @@ export function TranslationServiceProvider({ children }: { children: React.React translatedEventIdSet, getAccount, regenerateApiKey, - translate, + translateText, + translateEvent, getTranslatedEvent, showOriginalEvent, updateConfig diff --git a/src/services/libre-translate.service.ts b/src/services/libre-translate.service.ts index 3780b483..d17c5a3d 100644 --- a/src/services/libre-translate.service.ts +++ b/src/services/libre-translate.service.ts @@ -13,7 +13,10 @@ class LibreTranslateService { target: string, server?: string, api_key?: string - ): Promise { + ): Promise { + if (!text) { + return text + } if (!server) { throw new Error('LibreTranslate server address is not configured') } @@ -27,7 +30,11 @@ class LibreTranslateService { if (!response.ok) { throw new Error(data.error ?? 'Failed to translate') } - return data.translatedText + const translatedText = data.translatedText + if (!translatedText) { + throw new Error('Translation failed') + } + return translatedText } } diff --git a/src/services/translation.service.ts b/src/services/translation.service.ts index 2f565819..52af8e2a 100644 --- a/src/services/translation.service.ts +++ b/src/services/translation.service.ts @@ -60,14 +60,21 @@ class TranslationService { } } - async translate(text: string, target: string): Promise { + async translate(text: string, target: string): Promise { + if (!text) { + return text + } try { const data = await this._fetch({ path: '/v1/translation/translate', method: 'POST', body: JSON.stringify({ q: text, target }) }) - return data.translatedText + const translatedText = data.translatedText + if (!translatedText) { + throw new Error('Translation failed') + } + return translatedText } catch (error) { const errMsg = error instanceof Error ? error.message : '' throw new Error(errMsg || 'Failed to translate')