feat: support translation for profile abount
This commit is contained in:
@@ -42,7 +42,7 @@ export default function Nip05({ pubkey, append }: { pubkey: string; append?: str
|
||||
)}
|
||||
<SecondaryPageLink
|
||||
to={toNoteList({ domain: nip05Domain })}
|
||||
className={`hover:underline truncate ${nip05IsVerified ? 'text-primary' : 'text-muted-foreground'}`}
|
||||
className={`hover:underline truncate text-sm ${nip05IsVerified ? 'text-primary' : 'text-muted-foreground'}`}
|
||||
>
|
||||
{nip05Domain}
|
||||
</SecondaryPageLink>
|
||||
|
||||
@@ -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<string | null>(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 <EmbeddedMention key={index} userId={node.data.split(':')[1]} />
|
||||
}
|
||||
})
|
||||
}, [about])
|
||||
}, [about, translatedAbout])
|
||||
|
||||
return <div className={className}>{aboutNodes}</div>
|
||||
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 (
|
||||
<div>
|
||||
<div className={className}>{aboutNodes}</div>
|
||||
{needTranslation && (
|
||||
<div className="mt-2 text-sm">
|
||||
{translating ? (
|
||||
<div className="text-muted-foreground">{t('Translating...')}</div>
|
||||
) : translatedAbout === null ? (
|
||||
<button
|
||||
className="text-primary hover:underline"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
handleTranslate()
|
||||
}}
|
||||
>
|
||||
{t('Translate')}
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
className="text-primary hover:underline"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
handleShowOriginal()
|
||||
}}
|
||||
>
|
||||
{t('Show original')}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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': 'عرض الأصل'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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': '原文を表示'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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ł'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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': 'Показать оригинал'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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': 'แสดงต้นฉบับ'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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': '显示原文'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,12 +7,14 @@ import { createContext, useContext, useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useNostr } from './NostrProvider'
|
||||
|
||||
const translatedEventCache: Record<string, Event> = {}
|
||||
const translatedEventCache: Map<string, Event> = new Map()
|
||||
const translatedTextCache: Map<string, string> = new Map()
|
||||
|
||||
type TTranslationServiceContext = {
|
||||
config: TTranslationServiceConfig
|
||||
translatedEventIdSet: Set<string>
|
||||
translate: (event: Event) => Promise<Event | void>
|
||||
translateText: (text: string) => Promise<string>
|
||||
translateEvent: (event: Event) => Promise<Event | void>
|
||||
getTranslatedEvent: (eventId: string) => Event | null
|
||||
showOriginalEvent: (eventId: string) => void
|
||||
getAccount: () => Promise<TTranslationAccount | void>
|
||||
@@ -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<Event | void> => {
|
||||
const translate = async (text: string, target: string): Promise<string> => {
|
||||
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<string> => {
|
||||
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<Event> => {
|
||||
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<Event | void> => {
|
||||
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
|
||||
|
||||
@@ -13,7 +13,10 @@ class LibreTranslateService {
|
||||
target: string,
|
||||
server?: string,
|
||||
api_key?: string
|
||||
): Promise<string | undefined> {
|
||||
): Promise<string> {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -60,14 +60,21 @@ class TranslationService {
|
||||
}
|
||||
}
|
||||
|
||||
async translate(text: string, target: string): Promise<string | undefined> {
|
||||
async translate(text: string, target: string): Promise<string> {
|
||||
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')
|
||||
|
||||
Reference in New Issue
Block a user