From c697e8629db506e377bf2fb784434cd2f0eb8c13 Mon Sep 17 00:00:00 2001 From: codytseng Date: Mon, 28 Jul 2025 21:07:34 +0800 Subject: [PATCH] feat: add translation support for polls --- src/components/Note/Poll.tsx | 7 +- src/components/TranslateButton/index.tsx | 10 ++- src/providers/TranslationServiceProvider.tsx | 85 ++++++++++++++++---- 3 files changed, 83 insertions(+), 19 deletions(-) diff --git a/src/components/Note/Poll.tsx b/src/components/Note/Poll.tsx index 6785ae22..94a58190 100644 --- a/src/components/Note/Poll.tsx +++ b/src/components/Note/Poll.tsx @@ -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([]) 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) diff --git a/src/components/TranslateButton/index.tsx b/src/components/TranslateButton/index.tsx index c86f575d..850e3367 100644 --- a/src/components/TranslateButton/index.tsx +++ b/src/components/TranslateButton/index.tsx @@ -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] ) diff --git a/src/providers/TranslationServiceProvider.tsx b/src/providers/TranslationServiceProvider.tsx index b716d894..7902ef51 100644 --- a/src/providers/TranslationServiceProvider.tsx +++ b/src/providers/TranslationServiceProvider.tsx @@ -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 => { 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 => { + const target = i18n.language + const pollMetadata = getPollMetadataFromEvent(event) + + const texts: Record = { + question: event.content, + ...pollMetadata?.options.reduce( + (acc, option) => { + acc[option.id] = option.label + return acc + }, + {} as Record + ) + } + 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 => { @@ -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 ) } + +function joinTexts(texts: Record): 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 = {} + + 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 +}