feat: add translation support for polls

This commit is contained in:
codytseng
2025-07-28 21:07:34 +08:00
parent cd2c48dd5c
commit c697e8629d
3 changed files with 83 additions and 19 deletions

View File

@@ -1,5 +1,6 @@
import { Button } from '@/components/ui/button'
import { 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'
@@ -16,12 +17,16 @@ import { toast } from 'sonner'
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(event), [event])
const poll = useMemo(
() => getPollMetadataFromEvent(translatedEvent ?? event),
[event, translatedEvent]
)
const votedOptionIds = useMemo(() => {
if (!pollResults || !pubkey) return []
return Object.entries(pollResults.results)

View File

@@ -24,9 +24,13 @@ export default function TranslateButton({
const translatedEvent = useTranslatedEvent(event.id)
const supported = useMemo(
() =>
[kinds.ShortTextNote, kinds.Highlights, ExtendedKind.COMMENT, ExtendedKind.PICTURE].includes(
event.kind
),
[
kinds.ShortTextNote,
kinds.Highlights,
ExtendedKind.COMMENT,
ExtendedKind.PICTURE,
ExtendedKind.POLL
].includes(event.kind),
[event]
)

View File

@@ -1,3 +1,5 @@
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'
@@ -96,25 +98,51 @@ export function TranslationServiceProvider({ children }: { children: React.React
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
const texts = {
content: event.content,
comment
}
if (translatedComment) {
translatedEvent.tags = event.tags.map((tag) =>
tag[0] === 'comment' ? ['comment', translatedComment] : tag
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
)
}
setTranslatedEventIdSet((prev) => new Set(prev.add(event.id)))
return translatedEvent
}
const translateEvent = async (event: Event): Promise<Event | void> => {
@@ -134,6 +162,8 @@ export function TranslationServiceProvider({ children }: { children: React.React
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) {
@@ -178,3 +208,28 @@ export function TranslationServiceProvider({ children }: { children: React.React
</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
}