From 6b033f7509822e55928e70a986db1206ee38f439 Mon Sep 17 00:00:00 2001 From: Huang Xin Date: Tue, 27 May 2025 20:32:25 +0800 Subject: [PATCH] translator: post-process translated text for punctuation spacing (#1245) --- .../app/reader/hooks/useTextTranslation.ts | 38 +++++++++++++-- apps/readest-app/src/hooks/useTranslator.ts | 5 +- .../src/services/translators/index.ts | 1 + .../src/services/translators/polish.ts | 47 +++++++++++++++++++ .../src/services/translators/types.ts | 1 + apps/readest-app/src/utils/style.ts | 2 +- 6 files changed, 87 insertions(+), 7 deletions(-) create mode 100644 apps/readest-app/src/services/translators/polish.ts diff --git a/apps/readest-app/src/app/reader/hooks/useTextTranslation.ts b/apps/readest-app/src/app/reader/hooks/useTextTranslation.ts index feceed85..817739fd 100644 --- a/apps/readest-app/src/app/reader/hooks/useTextTranslation.ts +++ b/apps/readest-app/src/app/reader/hooks/useTextTranslation.ts @@ -57,18 +57,48 @@ export function useTextTranslation(bookKey: string, view: FoliateView | null) { const createTranslationObserver = () => { return new IntersectionObserver( (entries) => { + let beforeIntersectedElement: HTMLElement | null = null; + let lastIntersectedElement: HTMLElement | null = null; for (const entry of entries) { - if (!entry.isIntersecting) continue; - translateElement(entry.target as HTMLElement); + if (!entry.isIntersecting) { + if (!lastIntersectedElement) { + beforeIntersectedElement = entry.target as HTMLElement; + } + continue; + } + const currentElement = entry.target as HTMLElement; + translateElement(currentElement); + lastIntersectedElement = currentElement; + } + if (beforeIntersectedElement) { + translateElement(beforeIntersectedElement); + } + if (lastIntersectedElement) { + preTranslateNextElements(lastIntersectedElement, 2); } }, { - rootMargin: '2560px', - threshold: 0.01, + rootMargin: '1280px', + threshold: 0, }, ); }; + const preTranslateNextElements = (currentElement: HTMLElement, count: number) => { + if (!allTextNodes.current || count <= 0) return; + const currentIndex = allTextNodes.current.indexOf(currentElement); + if (currentIndex === -1) { + return; + } + + const nextElements = allTextNodes.current.slice(currentIndex + 1, currentIndex + 1 + count); + nextElements.forEach((element, index) => { + setTimeout(() => { + translateElement(element); + }, index * 500); + }); + }; + const recreateTranslationObserver = () => { const observer = createTranslationObserver(); observerRef.current?.disconnect(); diff --git a/apps/readest-app/src/hooks/useTranslator.ts b/apps/readest-app/src/hooks/useTranslator.ts index 0d5409ae..d9cf861c 100644 --- a/apps/readest-app/src/hooks/useTranslator.ts +++ b/apps/readest-app/src/hooks/useTranslator.ts @@ -1,12 +1,13 @@ import { useState, useCallback, useEffect } from 'react'; import { useAuth } from '@/context/AuthContext'; import { getTranslator, getTranslators } from '@/services/translators'; -import { getFromCache, storeInCache, UseTranslatorOptions } from '@/services/translators'; +import { getFromCache, storeInCache, polish, UseTranslatorOptions } from '@/services/translators'; export function useTranslator({ provider = 'deepl', sourceLang = 'AUTO', targetLang = 'EN', + enablePolishing = true, }: UseTranslatorOptions = {}) { const { token } = useAuth(); const [loading, setLoading] = useState(false); @@ -120,7 +121,7 @@ export function useTranslator({ ); setLoading(false); - return results; + return enablePolishing ? polish(results, targetLanguage) : results; } catch (err) { console.error('Translation error:', err); setLoading(false); diff --git a/apps/readest-app/src/services/translators/index.ts b/apps/readest-app/src/services/translators/index.ts index 7c1b4ea0..1f9b7b4c 100644 --- a/apps/readest-app/src/services/translators/index.ts +++ b/apps/readest-app/src/services/translators/index.ts @@ -1,3 +1,4 @@ export * from './types'; export * from './cache'; +export * from './polish'; export * from './providers'; diff --git a/apps/readest-app/src/services/translators/polish.ts b/apps/readest-app/src/services/translators/polish.ts new file mode 100644 index 00000000..1ed58443 --- /dev/null +++ b/apps/readest-app/src/services/translators/polish.ts @@ -0,0 +1,47 @@ +export type Polisher = (text: string) => string; + +const languagePolishers: Record = { + // Chinese - fix punctuation spacing + zh: (text: string) => + text + .replace(/--/g, '⸺') + .replace(/\s+([。、!?])/g, '$1') + .replace(/([。、!?])\s+/g, '$1'), + + // Spanish - fix punctuation spacing + es: (text: string) => + text.replace(/\?([A-ZÁÉÍÓÚÑÜ])/g, '? $1').replace(/\!([A-ZÁÉÍÓÚÑÜ])/g, '! $1'), + + // French - fix punctuation spacing + fr: (text: string) => text.replace(/\s+([!?:;])/g, ' $1').replace(/([!?:;])\s+/g, '$1 '), + + // Japanese - fix punctuation spacing + ja: (text: string) => text.replace(/\s+([。、!?])/g, '$1').replace(/([。、!?])\s+/g, '$1'), +}; + +export const basicPolish: Polisher = (text: string) => { + return text + .replace(/\s+/g, ' ') // Multiple spaces to single space + .replace(/\s+([.,!?;:])/g, '$1') // Remove space before punctuation + .trim(); +}; + +export function getPolisher(targetLang: string): Polisher { + const langCode = targetLang.split('-')[0]!.toLowerCase(); + const languagePolisher = languagePolishers[langCode]; + + if (languagePolisher) { + return (text: string) => { + const basicPolished = basicPolish(text); + const polished = languagePolisher(basicPolished); + return polished; + }; + } + + return basicPolish; +} + +export function polish(texts: string[], targetLang: string): string[] { + const polisher = getPolisher(targetLang); + return texts.map(polisher); +} diff --git a/apps/readest-app/src/services/translators/types.ts b/apps/readest-app/src/services/translators/types.ts index fad26e6b..7933c9a7 100644 --- a/apps/readest-app/src/services/translators/types.ts +++ b/apps/readest-app/src/services/translators/types.ts @@ -20,4 +20,5 @@ export interface UseTranslatorOptions { provider?: TranslatorName; sourceLang?: string; targetLang?: string; + enablePolishing?: boolean; } diff --git a/apps/readest-app/src/utils/style.ts b/apps/readest-app/src/utils/style.ts index aeecc629..8fadb7ad 100644 --- a/apps/readest-app/src/utils/style.ts +++ b/apps/readest-app/src/utils/style.ts @@ -359,7 +359,7 @@ export const getFootnoteStyles = () => ` const getTranslationStyles = () => ` .translation-block-wrapper { - display: inline-block !important; + display: block !important; margin: 0.5em 0 !important; } .translation-target.hidden {