Browse Source

remove the translation functionality

imwald
Silberengel 2 months ago
parent
commit
585f251bf4
  1. 5
      src/App.tsx
  2. 5
      src/components/Content/index.tsx
  3. 4
      src/components/ContentPreview/HighlightPreview.tsx
  4. 4
      src/components/ContentPreview/NormalContentPreview.tsx
  5. 9
      src/components/ContentPreview/PollPreview.tsx
  6. 7
      src/components/Note/Poll.tsx
  7. 2
      src/components/Note/index.tsx
  8. 115
      src/components/ProfileAbout/index.tsx
  9. 4
      src/components/RelayInfo/RelayReviewCard.tsx
  10. 2
      src/components/ReplyNote/index.tsx
  11. 94
      src/components/TranslateButton/index.tsx
  12. 1
      src/constants.ts
  13. 1
      src/hooks/index.tsx
  14. 21
      src/hooks/useTranslatedEvent.tsx
  15. 75
      src/pages/secondary/TranslationPage/JumbleTranslate/AccountInfo.tsx
  16. 87
      src/pages/secondary/TranslationPage/JumbleTranslate/JumbleTranslateAccountProvider.tsx
  17. 67
      src/pages/secondary/TranslationPage/JumbleTranslate/RegenerateApiKeyButton.tsx
  18. 164
      src/pages/secondary/TranslationPage/JumbleTranslate/TopUp.tsx
  19. 10
      src/pages/secondary/TranslationPage/JumbleTranslate/index.tsx
  20. 59
      src/pages/secondary/TranslationPage/LibreTranslate/index.tsx
  21. 83
      src/pages/secondary/TranslationPage/index.tsx
  22. 235
      src/providers/TranslationServiceProvider.tsx
  23. 5
      src/services/client.service.ts
  24. 42
      src/services/libre-translate.service.ts
  25. 21
      src/services/local-storage.service.ts
  26. 136
      src/services/translation.service.ts
  27. 16
      src/types/index.d.ts

5
src/App.tsx

@ -18,7 +18,6 @@ import { NostrProvider } from '@/providers/NostrProvider' @@ -18,7 +18,6 @@ import { NostrProvider } from '@/providers/NostrProvider'
import { ReplyProvider } from '@/providers/ReplyProvider'
import { ScreenSizeProvider } from '@/providers/ScreenSizeProvider'
import { ThemeProvider } from '@/providers/ThemeProvider'
import { TranslationServiceProvider } from '@/providers/TranslationServiceProvider'
import { UserPreferencesProvider } from '@/providers/UserPreferencesProvider'
import { UserTrustProvider } from '@/providers/UserTrustProvider'
import { ZapProvider } from '@/providers/ZapProvider'
@ -33,7 +32,6 @@ export default function App(): JSX.Element { @@ -33,7 +32,6 @@ export default function App(): JSX.Element {
<DeletedEventProvider>
<NostrProvider>
<ZapProvider>
<TranslationServiceProvider>
<FavoriteRelaysProvider>
<FollowListProvider>
<MuteListProvider>
@ -60,8 +58,7 @@ export default function App(): JSX.Element { @@ -60,8 +58,7 @@ export default function App(): JSX.Element {
</MuteListProvider>
</FollowListProvider>
</FavoriteRelaysProvider>
</TranslationServiceProvider>
</ZapProvider>
</ZapProvider>
</NostrProvider>
</DeletedEventProvider>
</ScreenSizeProvider>

5
src/components/Content/index.tsx

@ -1,4 +1,4 @@ @@ -1,4 +1,4 @@
import { useTranslatedEvent, useMediaExtraction } from '@/hooks'
import { useMediaExtraction } from '@/hooks'
import {
EmbeddedEmojiParser,
EmbeddedEventParser,
@ -78,8 +78,7 @@ export default function Content({ @@ -78,8 +78,7 @@ export default function Content({
className?: string
mustLoadMedia?: boolean
}) {
const translatedEvent = useTranslatedEvent(event?.id)
const _content = translatedEvent?.content ?? event?.content ?? content
const _content = event?.content ?? content
// Use unified media extraction service
const extractedMedia = useMediaExtraction(event, _content)

4
src/components/ContentPreview/HighlightPreview.tsx

@ -1,4 +1,3 @@ @@ -1,4 +1,3 @@
import { useTranslatedEvent } from '@/hooks'
import { getEmojiInfosFromEmojiTags } from '@/lib/tag'
import { cn } from '@/lib/utils'
import { Event } from 'nostr-tools'
@ -14,14 +13,13 @@ export default function HighlightPreview({ @@ -14,14 +13,13 @@ export default function HighlightPreview({
className?: string
}) {
const { t } = useTranslation()
const translatedEvent = useTranslatedEvent(event.id)
const emojiInfos = useMemo(() => getEmojiInfosFromEmojiTags(event.tags), [event])
return (
<div className={cn('pointer-events-none', className)}>
[{t('Highlight')}]{' '}
<Content
content={translatedEvent?.content ?? event.content}
content={event.content}
emojiInfos={emojiInfos}
className="italic pr-0.5"
/>

4
src/components/ContentPreview/NormalContentPreview.tsx

@ -1,4 +1,3 @@ @@ -1,4 +1,3 @@
import { useTranslatedEvent } from '@/hooks'
import { getEmojiInfosFromEmojiTags } from '@/lib/tag'
import { Event } from 'nostr-tools'
import { useMemo } from 'react'
@ -11,12 +10,11 @@ export default function NormalContentPreview({ @@ -11,12 +10,11 @@ export default function NormalContentPreview({
event: Event
className?: string
}) {
const translatedEvent = useTranslatedEvent(event?.id)
const emojiInfos = useMemo(() => getEmojiInfosFromEmojiTags(event?.tags), [event])
return (
<Content
content={translatedEvent?.content ?? event.content}
content={event.content}
className={className}
emojiInfos={emojiInfos}
/>

9
src/components/ContentPreview/PollPreview.tsx

@ -1,5 +1,4 @@ @@ -1,5 +1,4 @@
import { POLL_TYPE } from '@/constants'
import { useTranslatedEvent } from '@/hooks'
import { getPollMetadataFromEvent } from '@/lib/event-metadata'
import { getEmojiInfosFromEmojiTags } from '@/lib/tag'
import { cn } from '@/lib/utils'
@ -10,13 +9,9 @@ import Content from './Content' @@ -10,13 +9,9 @@ import Content from './Content'
export default function PollPreview({ event, className }: { event: Event; className?: string }) {
const { t } = useTranslation()
const translatedEvent = useTranslatedEvent(event.id)
const emojiInfos = useMemo(() => getEmojiInfosFromEmojiTags(event.tags), [event])
const poll = useMemo(
() => getPollMetadataFromEvent(translatedEvent ?? event),
[event, translatedEvent]
)
const content = (translatedEvent?.content ?? event.content)?.trim()
const poll = useMemo(() => getPollMetadataFromEvent(event), [event])
const content = event.content?.trim()
return (
<div className={cn('pointer-events-none', className)}>

7
src/components/Note/Poll.tsx

@ -1,6 +1,5 @@ @@ -1,6 +1,5 @@
import { Button } from '@/components/ui/button'
import { BIG_RELAY_URLS, POLL_TYPE } from '@/constants'
import { useTranslatedEvent } from '@/hooks'
import { useFetchPollResults } from '@/hooks/useFetchPollResults'
import { createPollResponseDraftEvent } from '@/lib/draft-event'
import { getPollMetadataFromEvent } from '@/lib/event-metadata'
@ -18,16 +17,12 @@ import { showPublishingFeedback, showSimplePublishSuccess } from '@/lib/publishi @@ -18,16 +17,12 @@ import { showPublishingFeedback, showSimplePublishSuccess } from '@/lib/publishi
export default function Poll({ event, className }: { event: Event; className?: string }) {
const { t } = useTranslation()
const translatedEvent = useTranslatedEvent(event.id)
const { pubkey, publish, startLogin } = useNostr()
const [isVoting, setIsVoting] = useState(false)
const [selectedOptionIds, setSelectedOptionIds] = useState<string[]>([])
const pollResults = useFetchPollResults(event.id)
const [isLoadingResults, setIsLoadingResults] = useState(false)
const poll = useMemo(
() => getPollMetadataFromEvent(translatedEvent ?? event),
[event, translatedEvent]
)
const poll = useMemo(() => getPollMetadataFromEvent(event), [event])
const votedOptionIds = useMemo(() => {
if (!pollResults || !pubkey) return []
return Object.entries(pollResults.results)

2
src/components/Note/index.tsx

@ -17,7 +17,6 @@ import { FormattedTimestamp } from '../FormattedTimestamp' @@ -17,7 +17,6 @@ import { FormattedTimestamp } from '../FormattedTimestamp'
import Nip05 from '../Nip05'
import NoteOptions from '../NoteOptions'
import ParentNotePreview from '../ParentNotePreview'
import TranslateButton from '../TranslateButton'
import UserAvatar from '../UserAvatar'
import Username from '../Username'
import { MessageSquare } from 'lucide-react'
@ -266,7 +265,6 @@ export default function Note({ @@ -266,7 +265,6 @@ export default function Note({
<MessageSquare className="w-4 h-4 text-blue-500" />
</button>
)}
<TranslateButton event={event} className={size === 'normal' ? '' : 'pr-0'} />
{size === 'normal' && (
<NoteOptions
event={event}

115
src/components/ProfileAbout/index.tsx

@ -5,11 +5,6 @@ import { @@ -5,11 +5,6 @@ import {
EmbeddedWebsocketUrlParser,
parseContent
} from '@/lib/content-parser'
import { detectLanguage } from '@/lib/utils'
import { useTranslationService } from '@/providers/TranslationServiceProvider'
import { useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { toast } from 'sonner'
import {
EmbeddedHashtag,
EmbeddedMention,
@ -18,94 +13,26 @@ import { @@ -18,94 +13,26 @@ import {
} from '../Embedded'
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 aboutNodes = parseContent(about ?? '', [
EmbeddedWebsocketUrlParser,
EmbeddedUrlParser,
EmbeddedHashtagParser,
EmbeddedMentionParser
]).map((node, index) => {
if (node.type === 'url') {
return <EmbeddedNormalUrl key={index} url={node.data} />
}
if (node.type === 'websocket-url') {
return <EmbeddedWebsocketUrl key={index} url={node.data} />
}
if (node.type === 'hashtag') {
return <EmbeddedHashtag key={index} hashtag={node.data} />
}
if (node.type === 'mention') {
return <EmbeddedMention key={index} userId={node.data.split(':')[1]} />
}
return node.data
})
const nodes = parseContent(translatedAbout ?? about, [
EmbeddedWebsocketUrlParser,
EmbeddedUrlParser,
EmbeddedHashtagParser,
EmbeddedMentionParser
])
return nodes.map((node, index) => {
if (node.type === 'url') {
return <EmbeddedNormalUrl key={index} url={node.data} />
}
if (node.type === 'websocket-url') {
return <EmbeddedWebsocketUrl key={index} url={node.data} />
}
if (node.type === 'hashtag') {
return <EmbeddedHashtag key={index} hashtag={node.data} />
}
if (node.type === 'mention') {
return <EmbeddedMention key={index} userId={node.data.split(':')[1]} />
}
return node.data
})
}, [about, translatedAbout])
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>
)
return <div className={className}>{aboutNodes}</div>
}

4
src/components/RelayInfo/RelayReviewCard.tsx

@ -9,7 +9,6 @@ import ContentPreview from '../ContentPreview' @@ -9,7 +9,6 @@ import ContentPreview from '../ContentPreview'
import { FormattedTimestamp } from '../FormattedTimestamp'
import Nip05 from '../Nip05'
import Stars from '../Stars'
import TranslateButton from '../TranslateButton'
import { SimpleUserAvatar } from '../UserAvatar'
import { SimpleUsername } from '../Username'
@ -53,9 +52,6 @@ export default function RelayReviewCard({ @@ -53,9 +52,6 @@ export default function RelayReviewCard({
</div>
</div>
</div>
<div className="flex items-center">
<TranslateButton event={event} className="pr-0" />
</div>
</div>
<Stars stars={stars} className="mt-2 gap-0.5 [&_svg]:size-3" />
<ContentPreview className="mt-2 line-clamp-4" event={event} />

2
src/components/ReplyNote/index.tsx

@ -17,7 +17,6 @@ import Nip05 from '../Nip05' @@ -17,7 +17,6 @@ import Nip05 from '../Nip05'
import NoteOptions from '../NoteOptions'
import NoteStats from '../NoteStats'
import ParentNotePreview from '../ParentNotePreview'
import TranslateButton from '../TranslateButton'
import UserAvatar from '../UserAvatar'
import Username from '../Username'
@ -94,7 +93,6 @@ export default function ReplyNote({ @@ -94,7 +93,6 @@ export default function ReplyNote({
</div>
</div>
<div className="flex items-center shrink-0">
<TranslateButton event={event} className="py-0" />
<NoteOptions event={event} className="shrink-0 [&_svg]:size-5" />
</div>
</div>

94
src/components/TranslateButton/index.tsx

@ -1,94 +0,0 @@ @@ -1,94 +0,0 @@
import { ExtendedKind } from '@/constants'
import { useTranslatedEvent } from '@/hooks'
import { toTranslation } from '@/lib/link'
import { cn, detectLanguage } from '@/lib/utils'
import { useSecondaryPage } from '@/PageManager'
import { useTranslationService } from '@/providers/TranslationServiceProvider'
import { Languages, Loader } from 'lucide-react'
import { Event, kinds } from 'nostr-tools'
import { useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { toast } from 'sonner'
export default function TranslateButton({
event,
className
}: {
event: Event
className?: string
}) {
const { i18n } = useTranslation()
const { push } = useSecondaryPage()
const { translateEvent, showOriginalEvent } = useTranslationService()
const [translating, setTranslating] = useState(false)
const translatedEvent = useTranslatedEvent(event.id)
const supported = useMemo(
() =>
[
kinds.ShortTextNote,
kinds.Highlights,
ExtendedKind.COMMENT,
ExtendedKind.PICTURE,
ExtendedKind.POLL,
ExtendedKind.RELAY_REVIEW
].includes(event.kind),
[event]
)
const needTranslation = useMemo(() => {
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) {
return null
}
const handleTranslate = async () => {
if (translating) return
setTranslating(true)
await translateEvent(event)
.catch((error) => {
toast.error(
'Translation failed: ' + (error.message || 'An error occurred while translating the note')
)
if (error.message === 'Insufficient balance.') {
push(toTranslation())
}
})
.finally(() => {
setTranslating(false)
})
}
const showOriginal = () => {
showOriginalEvent(event.id)
}
return (
<button
className={cn(
'flex items-center text-muted-foreground hover:text-pink-400 px-2 py-1 h-full disabled:text-muted-foreground [&_svg]:size-4 [&_svg]:shrink-0 transition-colors',
className
)}
disabled={translating}
onClick={(e) => {
e.stopPropagation()
if (translatedEvent) {
showOriginal()
} else {
handleTranslate()
}
}}
>
{translating ? (
<Loader className="animate-spin" />
) : (
<Languages className={translatedEvent ? 'text-pink-400 hover:text-pink-400/60' : ''} />
)}
</button>
)
}

1
src/constants.ts

@ -37,7 +37,6 @@ export const StorageKey = { @@ -37,7 +37,6 @@ export const StorageKey = {
AUTOPLAY: 'autoplay',
HIDE_UNTRUSTED_INTERACTIONS: 'hideUntrustedInteractions',
HIDE_UNTRUSTED_NOTIFICATIONS: 'hideUntrustedNotifications',
TRANSLATION_SERVICE_CONFIG_MAP: 'translationServiceConfigMap',
MEDIA_UPLOAD_SERVICE_CONFIG_MAP: 'mediaUploadServiceConfigMap',
HIDE_UNTRUSTED_NOTES: 'hideUntrustedNotes',
DEFAULT_SHOW_NSFW: 'defaultShowNsfw',

1
src/hooks/index.tsx

@ -6,5 +6,4 @@ export * from './useFetchRelayInfo' @@ -6,5 +6,4 @@ export * from './useFetchRelayInfo'
export * from './useFetchRelayInfos'
export * from './useFetchRelayList'
export * from './useSearchProfiles'
export * from './useTranslatedEvent'
export * from './useMediaExtraction'

21
src/hooks/useTranslatedEvent.tsx

@ -1,21 +0,0 @@ @@ -1,21 +0,0 @@
import { useTranslationService } from '@/providers/TranslationServiceProvider'
import { Event } from 'nostr-tools'
import { useEffect, useMemo, useState } from 'react'
export function useTranslatedEvent(eventId?: string) {
const { translatedEventIdSet, getTranslatedEvent } = useTranslationService()
const translated = useMemo(() => {
return eventId ? translatedEventIdSet.has(eventId) : false
}, [eventId, translatedEventIdSet])
const [translatedEvent, setTranslatedEvent] = useState<Event | null>(null)
useEffect(() => {
if (translated && eventId) {
setTranslatedEvent(getTranslatedEvent(eventId))
} else {
setTranslatedEvent(null)
}
}, [translated, eventId])
return translatedEvent
}

75
src/pages/secondary/TranslationPage/JumbleTranslate/AccountInfo.tsx

@ -1,75 +0,0 @@ @@ -1,75 +0,0 @@
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { JUMBLE_API_BASE_URL } from '@/constants'
import { useNostr } from '@/providers/NostrProvider'
import { Check, Copy, Eye, EyeOff } from 'lucide-react'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useJumbleTranslateAccount } from './JumbleTranslateAccountProvider'
import RegenerateApiKeyButton from './RegenerateApiKeyButton'
import TopUp from './TopUp'
export function AccountInfo() {
const { t } = useTranslation()
const { pubkey, startLogin } = useNostr()
const { account } = useJumbleTranslateAccount()
const [showApiKey, setShowApiKey] = useState(false)
const [copied, setCopied] = useState(false)
if (!pubkey) {
return (
<div className="w-full flex justify-center">
<Button onClick={() => startLogin()}>{t('Login')}</Button>
</div>
)
}
return (
<div className="space-y-4">
{/* Balance display in characters */}
<div className="space-y-2">
<p className="font-medium">{t('Balance')}</p>
<div className="flex items-baseline gap-2">
<p className="text-3xl font-bold">{account?.balance.toLocaleString() ?? '0'}</p>
<p className="text-muted-foreground">{t('characters')}</p>
</div>
</div>
{/* API Key section with visibility toggle and copy functionality */}
<div className="space-y-2">
<p className="font-medium">API key</p>
<div className="flex items-center gap-2">
<Input
type={showApiKey ? 'text' : 'password'}
value={account?.api_key ?? ''}
readOnly
className="font-mono flex-1 max-w-fit"
/>
<Button variant="outline" onClick={() => setShowApiKey(!showApiKey)}>
{showApiKey ? <Eye /> : <EyeOff />}
</Button>
<Button
variant="outline"
disabled={!account?.api_key}
onClick={() => {
if (!account?.api_key) return
navigator.clipboard.writeText(account.api_key)
setCopied(true)
setTimeout(() => setCopied(false), 4000)
}}
>
{copied ? <Check /> : <Copy />}
</Button>
<RegenerateApiKeyButton />
</div>
<p className="text-sm text-muted-foreground select-text">
{t('jumbleTranslateApiKeyDescription', {
serviceUrl: new URL('/v1/translation', JUMBLE_API_BASE_URL).toString()
})}
</p>
</div>
<TopUp />
<div className="h-40" />
</div>
)
}

87
src/pages/secondary/TranslationPage/JumbleTranslate/JumbleTranslateAccountProvider.tsx

@ -1,87 +0,0 @@ @@ -1,87 +0,0 @@
import { useNostr } from '@/providers/NostrProvider'
import { useTranslationService } from '@/providers/TranslationServiceProvider'
import { TTranslationAccount } from '@/types'
import { createContext, useContext, useEffect, useState } from 'react'
import { toast } from 'sonner'
type TJumbleTranslateAccountContext = {
account: TTranslationAccount | null
getAccount: () => Promise<void>
regenerateApiKey: () => Promise<void>
}
export const JumbleTranslateAccountContext = createContext<
TJumbleTranslateAccountContext | undefined
>(undefined)
export const useJumbleTranslateAccount = () => {
const context = useContext(JumbleTranslateAccountContext)
if (!context) {
throw new Error(
'useJumbleTranslateAccount must be used within a JumbleTranslateAccountProvider'
)
}
return context
}
export function JumbleTranslateAccountProvider({ children }: { children: React.ReactNode }) {
const { pubkey } = useNostr()
const { getAccount: _getAccount, regenerateApiKey: _regenerateApiKey } = useTranslationService()
const [account, setAccount] = useState<TTranslationAccount | null>(null)
useEffect(() => {
setAccount(null)
if (!pubkey) return
setTimeout(() => {
getAccount()
}, 100)
}, [pubkey])
const regenerateApiKey = async (): Promise<void> => {
try {
if (!account) {
await getAccount()
}
const newApiKey = await _regenerateApiKey()
if (newApiKey) {
setAccount((prev) => {
if (!prev) return prev
return {
...prev,
api_key: newApiKey
}
})
}
} catch (error) {
toast.error(
'Failed to regenerate Jumble translation API key: ' +
(error instanceof Error
? error.message
: 'An error occurred while regenerating the API key')
)
setAccount(null)
}
}
const getAccount = async (): Promise<void> => {
try {
const data = await _getAccount()
if (data) {
setAccount(data)
}
} catch (error) {
toast.error(
'Failed to fetch Jumble translation account: ' +
(error instanceof Error ? error.message : 'An error occurred while fetching the account')
)
setAccount(null)
}
}
return (
<JumbleTranslateAccountContext.Provider value={{ account, getAccount, regenerateApiKey }}>
{children}
</JumbleTranslateAccountContext.Provider>
)
}

67
src/pages/secondary/TranslationPage/JumbleTranslate/RegenerateApiKeyButton.tsx

@ -1,67 +0,0 @@ @@ -1,67 +0,0 @@
import { Button } from '@/components/ui/button'
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger
} from '@/components/ui/dialog'
import { Loader, RotateCcw } from 'lucide-react'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useJumbleTranslateAccount } from './JumbleTranslateAccountProvider'
export default function RegenerateApiKeyButton() {
const { t } = useTranslation()
const { account, regenerateApiKey } = useJumbleTranslateAccount()
const [resettingApiKey, setResettingApiKey] = useState(false)
const [showResetDialog, setShowResetDialog] = useState(false)
const handleRegenerateApiKey = async () => {
if (resettingApiKey || !account) return
setResettingApiKey(true)
await regenerateApiKey()
setShowResetDialog(false)
setResettingApiKey(false)
}
return (
<Dialog open={showResetDialog} onOpenChange={setShowResetDialog}>
<DialogTrigger asChild>
<Button variant="outline" disabled={!account?.api_key}>
<RotateCcw />
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>{t('Reset API key')}</DialogTitle>
<DialogDescription>
{t('Are you sure you want to reset your API key? This action cannot be undone.')}
<br />
<br />
<strong>{t('Warning')}:</strong>{' '}
{t(
'Your current API key will become invalid immediately, and any applications using it will stop working until you update them with the new key.'
)}
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button
variant="outline"
onClick={() => setShowResetDialog(false)}
disabled={resettingApiKey}
>
{t('Cancel')}
</Button>
<Button variant="destructive" onClick={handleRegenerateApiKey} disabled={resettingApiKey}>
{resettingApiKey && <Loader className="animate-spin" />}
{t('Reset API key')}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)
}

164
src/pages/secondary/TranslationPage/JumbleTranslate/TopUp.tsx

@ -1,164 +0,0 @@ @@ -1,164 +0,0 @@
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { cn } from '@/lib/utils'
import { useNostr } from '@/providers/NostrProvider'
import transaction from '@/services/transaction.service'
import { closeModal, launchPaymentModal } from '@getalby/bitcoin-connect-react'
import { Loader } from 'lucide-react'
import { useState } from 'react'
import { toast } from 'sonner'
import { useJumbleTranslateAccount } from './JumbleTranslateAccountProvider'
import { useTranslation } from 'react-i18next'
export default function TopUp() {
const { t } = useTranslation()
const { pubkey } = useNostr()
const { getAccount } = useJumbleTranslateAccount()
const [topUpLoading, setTopUpLoading] = useState(false)
const [topUpAmount, setTopUpAmount] = useState(1000)
const [selectedAmount, setSelectedAmount] = useState<number | null>(1000)
const presetAmounts = [
{ amount: 1_000, text: '1k' },
{ amount: 5_000, text: '5k' },
{ amount: 10_000, text: '10k' },
{ amount: 25_000, text: '25k' },
{ amount: 50_000, text: '50k' },
{ amount: 100_000, text: '100k' }
]
const charactersPerUnit = 100 // 1 unit = 100 characters
const calculateCharacters = (amount: number) => {
return amount * charactersPerUnit
}
const handlePresetClick = (amount: number) => {
setSelectedAmount(amount)
setTopUpAmount(amount)
}
const handleInputChange = (value: string) => {
const numValue = parseInt(value) || 0
setTopUpAmount(numValue)
setSelectedAmount(numValue >= 1000 ? numValue : null)
}
const handleTopUp = async (amount: number | null) => {
if (topUpLoading || !pubkey || !amount || amount < 1000) return
setTopUpLoading(true)
try {
const { transactionId, invoiceId } = await transaction.createTransaction(pubkey, amount)
let checkPaymentInterval: ReturnType<typeof setInterval> | undefined = undefined
const { setPaid } = launchPaymentModal({
invoice: invoiceId,
onCancelled: () => {
clearInterval(checkPaymentInterval)
setTopUpLoading(false)
}
})
let failedCount = 0
checkPaymentInterval = setInterval(async () => {
try {
const { state } = await transaction.checkTransaction(transactionId)
if (state === 'pending') return
clearInterval(checkPaymentInterval)
setTopUpLoading(false)
if (state === 'settled') {
setPaid({ preimage: '' }) // Preimage is not returned, but we can assume payment is successful
getAccount() // Refresh account balance
} else {
closeModal()
toast.error('The invoice has expired or the payment was not successful')
}
} catch (err) {
failedCount++
if (failedCount <= 3) return
clearInterval(checkPaymentInterval)
setTopUpLoading(false)
toast.error(
'Top up failed: ' +
(err instanceof Error ? err.message : 'An error occurred while topping up')
)
}
}, 2000)
} catch (err) {
setTopUpLoading(false)
toast.error(
'Top up failed: ' +
(err instanceof Error ? err.message : 'An error occurred while topping up')
)
}
}
return (
<div className="space-y-4">
<p className="font-medium">{t('Top up')}</p>
{/* Preset amounts */}
<div className="grid grid-cols-2 gap-2">
{presetAmounts.map(({ amount, text }) => (
<Button
key={amount}
variant="outline"
onClick={() => handlePresetClick(amount)}
className={cn(
'flex flex-col h-auto py-3 hover:bg-primary/10',
selectedAmount === amount && 'border border-primary bg-primary/10'
)}
>
<span className="text-lg font-semibold">
{text} {t('sats')}
</span>
<span className="text-sm text-muted-foreground">
{calculateCharacters(amount).toLocaleString()} {t('characters')}
</span>
</Button>
))}
</div>
{/* Custom amount input */}
<div className="space-y-2">
<div className="flex items-center gap-2">
<Input
type="number"
placeholder="Custom amount"
value={topUpAmount}
onChange={(e) => handleInputChange(e.target.value)}
min={1000}
step={1000}
className="w-40"
/>
<span className="text-sm text-muted-foreground">{t('sats')}</span>
</div>
{selectedAmount && selectedAmount >= 1000 && (
<p className="text-sm text-muted-foreground">
{t('Will receive: {n} characters', {
n: calculateCharacters(selectedAmount).toLocaleString()
})}
</p>
)}
</div>
<Button
className="w-full"
disabled={topUpLoading || !selectedAmount || selectedAmount < 1000}
onClick={() => handleTopUp(selectedAmount)}
>
{topUpLoading && <Loader className="animate-spin" />}
{selectedAmount && selectedAmount >= 1000
? t('Top up {n} sats', {
n: selectedAmount?.toLocaleString()
})
: t('Minimum top up is {n} sats', {
n: new Number(1000).toLocaleString()
})}
</Button>
</div>
)
}

10
src/pages/secondary/TranslationPage/JumbleTranslate/index.tsx

@ -1,10 +0,0 @@ @@ -1,10 +0,0 @@
import { AccountInfo } from './AccountInfo'
import { JumbleTranslateAccountProvider } from './JumbleTranslateAccountProvider'
export default function JumbleTranslate() {
return (
<JumbleTranslateAccountProvider>
<AccountInfo />
</JumbleTranslateAccountProvider>
)
}

59
src/pages/secondary/TranslationPage/LibreTranslate/index.tsx

@ -1,59 +0,0 @@ @@ -1,59 +0,0 @@
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { useTranslationService } from '@/providers/TranslationServiceProvider'
import { useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
export default function LibreTranslate() {
const { t } = useTranslation()
const { config, updateConfig } = useTranslationService()
const [server, setServer] = useState(
config.service === 'libre_translate' ? (config.server ?? '') : ''
)
const [apiKey, setApiKey] = useState(
config.service === 'libre_translate' ? (config.api_key ?? '') : ''
)
const initialized = useRef(false)
useEffect(() => {
if (!initialized.current) {
initialized.current = true
return
}
updateConfig({
service: 'libre_translate',
server,
api_key: apiKey
})
}, [server, apiKey])
return (
<div className="space-y-4">
<div className="space-y-2">
<Label htmlFor="libre-translate-server" className="text-base">
{t('Service address')}
</Label>
<Input
id="libre-translate-server"
type="text"
value={server}
onChange={(e) => setServer(e.target.value)}
placeholder="Enter server address"
/>
</div>
<div className="space-y-2">
<Label htmlFor="libre-translate-api-key" className="text-base">
API key
</Label>
<Input
id="libre-translate-api-key"
type="text"
value={apiKey}
onChange={(e) => setApiKey(e.target.value)}
placeholder="Enter API Key"
/>
</div>
</div>
)
}

83
src/pages/secondary/TranslationPage/index.tsx

@ -1,74 +1,23 @@ @@ -1,74 +1,23 @@
import { Label } from '@/components/ui/label'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue
} from '@/components/ui/select'
import { LocalizedLanguageNames } from '@/i18n'
import SecondaryPageLayout from '@/layouts/SecondaryPageLayout'
import { useTranslationService } from '@/providers/TranslationServiceProvider'
import { TLanguage } from '@/types'
import { forwardRef, useState } from 'react'
import { forwardRef } from 'react'
import { useTranslation } from 'react-i18next'
import JumbleTranslate from './JumbleTranslate'
import LibreTranslate from './LibreTranslate'
const TranslationPage = forwardRef(({ index, hideTitlebar = false }: { index?: number; hideTitlebar?: boolean }, ref) => {
const { t, i18n } = useTranslation()
const { config, updateConfig } = useTranslationService()
const [language, setLanguage] = useState<TLanguage>(i18n.language as TLanguage)
const TranslationPage = forwardRef(
({ index, hideTitlebar = false }: { index?: number; hideTitlebar?: boolean }, ref) => {
const { t } = useTranslation()
const handleLanguageChange = (value: TLanguage) => {
i18n.changeLanguage(value)
setLanguage(value)
}
return (
<SecondaryPageLayout ref={ref} index={index} title={hideTitlebar ? undefined : t('Translation')}>
<div className="px-4 pt-3 space-y-4">
<div className="space-y-2">
<Label htmlFor="languages" className="text-base font-medium">
{t('Languages')}
</Label>
<Select defaultValue="en" value={language} onValueChange={handleLanguageChange}>
<SelectTrigger id="languages" className="w-48">
<SelectValue />
</SelectTrigger>
<SelectContent>
{Object.entries(LocalizedLanguageNames).map(([key, value]) => (
<SelectItem key={key} value={key}>
{value}
</SelectItem>
))}
</SelectContent>
</Select>
return (
<SecondaryPageLayout ref={ref} index={index} title={hideTitlebar ? undefined : t('Translation')}>
<div className="px-4 pt-3 space-y-4">
<p className="text-muted-foreground">
{t(
'To translate notes and other content, use your browser’s built-in translation. For example: right-click the page and choose “Translate to…”, or use the translate icon in the address bar.'
)}
</p>
</div>
<div className="space-y-2">
<Label htmlFor="translation-service-select" className="text-base font-medium">
{t('Service')}
</Label>
<Select
defaultValue={config.service}
value={config.service}
onValueChange={(newService) => {
updateConfig({ service: newService as 'jumble' | 'libre_translate' })
}}
>
<SelectTrigger id="translation-service-select" className="w-[180px]">
<SelectValue placeholder={t('Select Translation Service')} />
</SelectTrigger>
<SelectContent>
<SelectItem value="jumble">Jumble</SelectItem>
<SelectItem value="libre_translate">LibreTranslate</SelectItem>
</SelectContent>
</Select>
</div>
{config.service === 'jumble' ? <JumbleTranslate /> : <LibreTranslate />}
</div>
</SecondaryPageLayout>
)
})
</SecondaryPageLayout>
)
}
)
TranslationPage.displayName = 'TranslationPage'
export default TranslationPage

235
src/providers/TranslationServiceProvider.tsx

@ -1,235 +0,0 @@ @@ -1,235 +0,0 @@
import { ExtendedKind } from '@/constants'
import { getPollMetadataFromEvent } from '@/lib/event-metadata'
import libreTranslate from '@/services/libre-translate.service'
import storage from '@/services/local-storage.service'
import translation from '@/services/translation.service'
import { TTranslationAccount, TTranslationServiceConfig } from '@/types'
import { Event, kinds } from 'nostr-tools'
import { createContext, useContext, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useNostr } from './NostrProvider'
const translatedEventCache: Map<string, Event> = new Map()
const translatedTextCache: Map<string, string> = new Map()
type TTranslationServiceContext = {
config: TTranslationServiceConfig
translatedEventIdSet: Set<string>
translateText: (text: string) => Promise<string>
translateEvent: (event: Event) => Promise<Event | void>
getTranslatedEvent: (eventId: string) => Event | null
showOriginalEvent: (eventId: string) => void
getAccount: () => Promise<TTranslationAccount | void>
regenerateApiKey: () => Promise<string | undefined>
updateConfig: (newConfig: TTranslationServiceConfig) => void
}
const TranslationServiceContext = createContext<TTranslationServiceContext | undefined>(undefined)
export const useTranslationService = () => {
const context = useContext(TranslationServiceContext)
if (!context) {
throw new Error('useTranslation must be used within a TranslationProvider')
}
return context
}
export function TranslationServiceProvider({ children }: { children: React.ReactNode }) {
const { i18n } = useTranslation()
const [config, setConfig] = useState<TTranslationServiceConfig>({ service: 'jumble' })
const { pubkey, startLogin } = useNostr()
const [translatedEventIdSet, setTranslatedEventIdSet] = useState<Set<string>>(new Set())
useEffect(() => {
translation.changeCurrentPubkey(pubkey)
const config = storage.getTranslationServiceConfig(pubkey)
setConfig(config)
}, [pubkey])
const getAccount = async (): Promise<TTranslationAccount | void> => {
if (config.service !== 'jumble') return
if (!pubkey) {
startLogin()
return
}
return await translation.getAccount()
}
const regenerateApiKey = async (): Promise<string | undefined> => {
if (config.service !== 'jumble') return
if (!pubkey) {
startLogin()
return
}
return await translation.regenerateApiKey()
}
const getTranslatedEvent = (eventId: string): Event | null => {
const target = i18n.language
const cacheKey = target + '_' + eventId
return translatedEventCache.get(cacheKey) ?? null
}
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]
const texts = {
content: event.content,
comment
}
const joinedText = joinTexts(texts)
if (!joinedText) return event
const translatedText = await translate(joinedText, target)
const translatedTexts = splitTranslatedText(translatedText)
return {
...event,
content: translatedTexts.content ?? event.content,
tags: event.tags.map((tag) =>
tag[0] === 'comment' ? ['comment', translatedTexts.comment ?? tag[1]] : tag
)
}
}
const translatePollEvent = async (event: Event): Promise<Event> => {
const target = i18n.language
const pollMetadata = getPollMetadataFromEvent(event)
const texts: Record<string, string> = {
question: event.content,
...pollMetadata?.options.reduce(
(acc, option) => {
acc[option.id] = option.label
return acc
},
{} as Record<string, string>
)
}
const joinedText = joinTexts(texts)
if (!joinedText) return event
const translatedText = await translate(joinedText, target)
const translatedTexts = splitTranslatedText(translatedText)
return {
...event,
content: translatedTexts.question ?? '',
tags: event.tags.map((tag) =>
tag[0] === 'option' ? ['option', tag[1], translatedTexts[tag[1]] ?? tag[2]] : tag
)
}
}
const translateEvent = async (event: Event): Promise<Event | void> => {
if (config.service === 'jumble' && !pubkey) {
startLogin()
return
}
const target = i18n.language
const cacheKey = target + '_' + event.id
const cache = translatedEventCache.get(cacheKey)
if (cache) {
setTranslatedEventIdSet((prev) => new Set(prev.add(event.id)))
return cache
}
let translatedEvent: Event | undefined
if (event.kind === kinds.Highlights) {
translatedEvent = await translateHighlightEvent(event)
} else if (event.kind === ExtendedKind.POLL) {
translatedEvent = await translatePollEvent(event)
} else {
const translatedText = await translate(event.content, target)
if (!translatedText) {
return
}
translatedEvent = { ...event, content: translatedText }
}
translatedEventCache.set(cacheKey, translatedEvent)
setTranslatedEventIdSet((prev) => new Set(prev.add(event.id)))
return translatedEvent
}
const showOriginalEvent = (eventId: string) => {
setTranslatedEventIdSet((prev) => {
const newSet = new Set(prev)
newSet.delete(eventId)
return newSet
})
}
const updateConfig = (newConfig: TTranslationServiceConfig) => {
setConfig(newConfig)
storage.setTranslationServiceConfig(newConfig, pubkey)
}
return (
<TranslationServiceContext.Provider
value={{
config,
translatedEventIdSet,
getAccount,
regenerateApiKey,
translateText,
translateEvent,
getTranslatedEvent,
showOriginalEvent,
updateConfig
}}
>
{children}
</TranslationServiceContext.Provider>
)
}
function joinTexts(texts: Record<string, string | undefined>): string {
return (
Object.entries(texts).filter(([, content]) => content && content.trim() !== '') as [
string,
string
][]
)
.map(([key, content]) => `=== ${key} ===\n${content.trim()}\n=== ${key} ===`)
.join('\n\n')
}
function splitTranslatedText(translated: string) {
const regex = /=== (.+?) ===\n([\s\S]*?)\n=== \1 ===/g
const results: Record<string, string | undefined> = {}
let match: RegExpExecArray | null
while ((match = regex.exec(translated)) !== null) {
const key = match[1].trim()
const content = match[2].trim()
results[key] = content
}
return results
}

5
src/services/client.service.ts

@ -2152,11 +2152,6 @@ class ClientService extends EventTarget { @@ -2152,11 +2152,6 @@ class ClientService extends EventTarget {
await this.updateReplaceableEventCache(evt)
}
/** Fetch profile (kind 0) event; uses replaceable cache and IndexedDB. */
async fetchProfileEvent(pubkey: string) {
return await this.fetchReplaceableEvent(pubkey, kinds.Metadata)
}
/**
* Force-refresh profile (kind 0) and payment info (kind 10133) cache for a pubkey:
* clears in-memory cache and IndexedDB so the next fetch loads from relays.

42
src/services/libre-translate.service.ts

@ -1,42 +0,0 @@ @@ -1,42 +0,0 @@
class LibreTranslateService {
static instance: LibreTranslateService
constructor() {
if (!LibreTranslateService.instance) {
LibreTranslateService.instance = this
}
return LibreTranslateService.instance
}
async translate(
text: string,
target: string,
server?: string,
api_key?: string
): Promise<string> {
if (!text) {
return text
}
if (!server) {
throw new Error('LibreTranslate server address is not configured')
}
const url = new URL('/translate', server).toString()
const response = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ q: text, target, source: 'auto', api_key })
})
const data = await response.json()
if (!response.ok) {
throw new Error(data.error ?? 'Failed to translate')
}
const translatedText = data.translatedText
if (!translatedText) {
throw new Error('Translation failed')
}
return translatedText
}
}
const instance = new LibreTranslateService()
export default instance

21
src/services/local-storage.service.ts

@ -20,7 +20,6 @@ import { @@ -20,7 +20,6 @@ import {
TNotificationStyle,
TRelaySet,
TThemeSetting,
TTranslationServiceConfig
} from '@/types'
class LocalStorageService {
@ -43,7 +42,6 @@ class LocalStorageService { @@ -43,7 +42,6 @@ class LocalStorageService {
private hideUntrustedInteractions: boolean = false
private hideUntrustedNotifications: boolean = false
private hideUntrustedNotes: boolean = false
private translationServiceConfigMap: Record<string, TTranslationServiceConfig> = {}
private mediaUploadServiceConfigMap: Record<string, TMediaUploadServiceConfig> = {}
private defaultShowNsfw: boolean = false
private dismissedTooManyRelaysAlert: boolean = false
@ -162,13 +160,6 @@ class LocalStorageService { @@ -162,13 +160,6 @@ class LocalStorageService {
? storedHideUntrustedNotes === 'true'
: hideUntrustedEvents
const translationServiceConfigMapStr = window.localStorage.getItem(
StorageKey.TRANSLATION_SERVICE_CONFIG_MAP
)
if (translationServiceConfigMapStr) {
this.translationServiceConfigMap = JSON.parse(translationServiceConfigMapStr)
}
const mediaUploadServiceConfigMapStr = window.localStorage.getItem(
StorageKey.MEDIA_UPLOAD_SERVICE_CONFIG_MAP
)
@ -546,18 +537,6 @@ class LocalStorageService { @@ -546,18 +537,6 @@ class LocalStorageService {
window.localStorage.setItem(StorageKey.HIDE_UNTRUSTED_NOTES, hideUntrustedNotes.toString())
}
getTranslationServiceConfig(pubkey?: string | null) {
return this.translationServiceConfigMap[pubkey ?? '_'] ?? { service: 'jumble' }
}
setTranslationServiceConfig(config: TTranslationServiceConfig, pubkey?: string | null) {
this.translationServiceConfigMap[pubkey ?? '_'] = config
window.localStorage.setItem(
StorageKey.TRANSLATION_SERVICE_CONFIG_MAP,
JSON.stringify(this.translationServiceConfigMap)
)
}
getMediaUploadServiceConfig(pubkey?: string | null): TMediaUploadServiceConfig {
const defaultConfig = { type: 'nip96', service: this.mediaUploadService } as const
if (!pubkey) {

136
src/services/translation.service.ts

@ -1,136 +0,0 @@ @@ -1,136 +0,0 @@
import { JUMBLE_API_BASE_URL } from '@/constants'
import client from '@/services/client.service'
import { TTranslationAccount } from '@/types'
class TranslationService {
static instance: TranslationService
private apiKeyMap: Record<string, string | undefined> = {}
private currentPubkey: string | null = null
constructor() {
if (!TranslationService.instance) {
TranslationService.instance = this
}
return TranslationService.instance
}
async getAccount(): Promise<TTranslationAccount> {
if (!this.currentPubkey) {
throw new Error('Please login first')
}
const apiKey = this.apiKeyMap[this.currentPubkey]
const path = '/v1/translation/account'
const method = 'GET'
let auth: string | undefined
if (!apiKey) {
auth = await client.signHttpAuth(
new URL(path, JUMBLE_API_BASE_URL).toString(),
method,
'Auth to get Jumble translation service account'
)
}
const act = await this._fetch<TTranslationAccount>({
path,
method,
auth,
retryWhenUnauthorized: !auth
})
if (act.api_key && act.pubkey) {
this.apiKeyMap[act.pubkey] = act.api_key
}
return act
}
async regenerateApiKey(): Promise<string> {
try {
const data = await this._fetch({
path: '/v1/translation/regenerate-api-key',
method: 'POST'
})
if (data.api_key && this.currentPubkey) {
this.apiKeyMap[this.currentPubkey] = data.api_key
}
return data.api_key
} catch (error) {
const errMsg = error instanceof Error ? error.message : ''
throw new Error(errMsg || 'Failed to regenerate API key')
}
}
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 })
})
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')
}
}
changeCurrentPubkey(pubkey: string | null): void {
this.currentPubkey = pubkey
}
private async _fetch<T = any>({
path,
method,
body,
auth,
retryWhenUnauthorized = true
}: {
path: string
method: string
body?: string
auth?: string
retryWhenUnauthorized?: boolean
}): Promise<T> {
if (!this.currentPubkey) {
throw new Error('Please login first')
}
const apiKey = this.apiKeyMap[this.currentPubkey]
const hasApiKey = !!apiKey
let _auth: string
if (auth) {
_auth = auth
} else if (hasApiKey) {
_auth = `Bearer ${apiKey}`
} else {
const act = await this.getAccount()
_auth = `Bearer ${act.api_key}`
}
const url = new URL(path, JUMBLE_API_BASE_URL).toString()
const response = await fetch(url, {
method,
headers: { 'Content-Type': 'application/json', Authorization: _auth },
body
})
const data = await response.json()
if (!response.ok) {
if (data.code === '00403' && hasApiKey && retryWhenUnauthorized) {
this.apiKeyMap[this.currentPubkey] = undefined
return this._fetch({ path, method, body, retryWhenUnauthorized: false })
}
throw new Error(data.error)
}
return data
}
}
const instance = new TranslationService()
export default instance

16
src/types/index.d.ts vendored

@ -186,22 +186,6 @@ export type TEmoji = { @@ -186,22 +186,6 @@ export type TEmoji = {
url: string
}
export type TTranslationAccount = {
pubkey: string
api_key: string
balance: number
}
export type TTranslationServiceConfig =
| {
service: 'jumble'
}
| {
service: 'libre_translate'
server?: string
api_key?: string
}
export type TMediaUploadServiceConfig =
| {
type: 'nip96'

Loading…
Cancel
Save