forked from akai/readest
This commit is contained in:
@@ -7,20 +7,14 @@ import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { useTranslator } from '@/hooks/useTranslator';
|
||||
import { TRANSLATED_LANGS } from '@/services/constants';
|
||||
import { UseTranslatorOptions, getTranslators } from '@/services/translators';
|
||||
import { localeToLang } from '@/utils/lang';
|
||||
import Select from '@/components/Select';
|
||||
|
||||
const notSupportedLangs = ['hi', 'vi'];
|
||||
const notSupportedLangs = [''];
|
||||
|
||||
const generateTranslatorLangs = () => {
|
||||
const langs = { ...TRANSLATED_LANGS };
|
||||
const result: Record<string, string> = {};
|
||||
for (const [code, name] of Object.entries(langs)) {
|
||||
if (notSupportedLangs.includes(code)) continue;
|
||||
const newCode = localeToLang(code).toUpperCase();
|
||||
result[newCode] = name;
|
||||
}
|
||||
return result;
|
||||
return Object.fromEntries(
|
||||
Object.entries(TRANSLATED_LANGS).filter(([code]) => !notSupportedLangs.includes(code)),
|
||||
);
|
||||
};
|
||||
|
||||
const TRANSLATOR_LANGS = generateTranslatorLangs();
|
||||
@@ -205,7 +199,7 @@ const TranslatorPopup: React.FC<TranslatorPopupProps> = ({
|
||||
</div>
|
||||
<div className='absolute bottom-0 flex h-8 w-full items-center justify-between bg-gray-600 px-4'>
|
||||
{provider && !loading && (
|
||||
<div className='text-xs opacity-60'>
|
||||
<div className='line-clamp-1 text-xs opacity-60'>
|
||||
{error
|
||||
? ''
|
||||
: _('Translated by {{provider}}.', {
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { FoliateView } from '@/types/view';
|
||||
import { UseTranslatorOptions } from '@/services/translators';
|
||||
import { useReaderStore } from '@/store/readerStore';
|
||||
import { useTranslator } from '@/hooks/useTranslator';
|
||||
import { walkTextNodes } from '@/utils/walk';
|
||||
import { localeToLang } from '@/utils/lang';
|
||||
import { debounce } from '@/utils/debounce';
|
||||
import { getLocale } from '@/utils/misc';
|
||||
|
||||
export function useTextTranslation(bookKey: string, view: FoliateView | HTMLElement | null) {
|
||||
const { getViewSettings } = useReaderStore();
|
||||
const { getViewSettings, getViewState, getProgress } = useReaderStore();
|
||||
const viewSettings = getViewSettings(bookKey);
|
||||
const viewState = getViewState(bookKey);
|
||||
const progress = getProgress(bookKey);
|
||||
|
||||
const enabled = useRef(viewSettings?.translationEnabled);
|
||||
const [provider, setProvider] = useState(viewSettings?.translationProvider);
|
||||
@@ -17,7 +19,7 @@ export function useTextTranslation(bookKey: string, view: FoliateView | HTMLElem
|
||||
|
||||
const { translate } = useTranslator({
|
||||
provider,
|
||||
targetLang: localeToLang(targetLang || getLocale()),
|
||||
targetLang: targetLang || getLocale(),
|
||||
} as UseTranslatorOptions);
|
||||
|
||||
const translateRef = useRef(translate);
|
||||
@@ -118,7 +120,7 @@ export function useTextTranslation(bookKey: string, view: FoliateView | HTMLElem
|
||||
|
||||
const translateElement = async (el: HTMLElement) => {
|
||||
if (!enabled.current) return;
|
||||
const text = el.textContent?.trim();
|
||||
const text = el.textContent?.replaceAll('\n', '').trim();
|
||||
if (!text) return;
|
||||
|
||||
if (el.querySelector('.translation-target')) {
|
||||
@@ -156,6 +158,61 @@ export function useTextTranslation(bookKey: string, view: FoliateView | HTMLElem
|
||||
}
|
||||
};
|
||||
|
||||
const findNodeIndicesInRange = (range: Range, nodes: HTMLElement[]) => {
|
||||
const startContainer = range.startContainer;
|
||||
const endContainer = range.endContainer;
|
||||
|
||||
let startIndex = -1;
|
||||
let endIndex = -1;
|
||||
for (let i = 0; i < nodes.length; i++) {
|
||||
const node = nodes[i]!;
|
||||
if (node === startContainer || node.contains(startContainer)) {
|
||||
if (startIndex === -1) startIndex = i;
|
||||
}
|
||||
if (node === endContainer || node.contains(endContainer)) {
|
||||
endIndex = i;
|
||||
}
|
||||
}
|
||||
if (startIndex !== -1 && endIndex === -1) {
|
||||
endIndex = startIndex;
|
||||
}
|
||||
|
||||
return { startIndex, endIndex };
|
||||
};
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
const translateInRange = useCallback(
|
||||
debounce((range: Range) => {
|
||||
const nodes = allTextNodes.current;
|
||||
if (nodes.length === 0) {
|
||||
console.warn('No text nodes available for translation.');
|
||||
return;
|
||||
}
|
||||
const { startIndex, endIndex } = findNodeIndicesInRange(range, nodes);
|
||||
if (startIndex === -1) {
|
||||
console.log('Range not found in text nodes');
|
||||
return;
|
||||
}
|
||||
const beforeStart = Math.max(0, startIndex - 2);
|
||||
const afterEnd = Math.min(nodes.length - 1, endIndex + 2);
|
||||
for (let i = beforeStart; i <= afterEnd; i++) {
|
||||
const node = nodes[i];
|
||||
if (node) {
|
||||
translateElement(node);
|
||||
}
|
||||
}
|
||||
}, 500),
|
||||
[],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (viewState?.ttsEnabled && progress && document.hidden) {
|
||||
const { range } = progress;
|
||||
translateInRange(range);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [viewState?.ttsEnabled, progress]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!viewSettings) return;
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import { stubTranslation as _ } from '@/utils/misc';
|
||||
import { fetch as tauriFetch } from '@tauri-apps/plugin-http';
|
||||
import { isTauriAppPlatform } from '@/services/environment';
|
||||
import { TranslationProvider } from '../types';
|
||||
import { langToDefaultLocale } from '@/utils/lang';
|
||||
import { normalizeToFullLang } from '@/utils/lang';
|
||||
|
||||
interface TokenCache {
|
||||
token: string;
|
||||
@@ -53,8 +53,8 @@ export const azureProvider: TranslationProvider = {
|
||||
if (!text.length) return [];
|
||||
|
||||
const results: string[] = [];
|
||||
const msSourceLang = sourceLang ? langToDefaultLocale(sourceLang.toLowerCase()) : '';
|
||||
const msTargetLang = langToDefaultLocale(targetLang.toLowerCase());
|
||||
const msSourceLang = sourceLang ? normalizeToFullLang(sourceLang) : '';
|
||||
const msTargetLang = normalizeToFullLang(targetLang);
|
||||
|
||||
const translationPromises = text.map(async (line, index) => {
|
||||
if (!line?.trim().length) {
|
||||
@@ -67,7 +67,7 @@ export const azureProvider: TranslationProvider = {
|
||||
to: msTargetLang,
|
||||
'api-version': '3.0',
|
||||
});
|
||||
if (msSourceLang && msSourceLang !== 'auto') {
|
||||
if (msSourceLang && msSourceLang.toLowerCase() !== 'auto') {
|
||||
params.append('from', msSourceLang);
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import { stubTranslation as _ } from '@/utils/misc';
|
||||
import { ErrorCodes, TranslationProvider } from '../types';
|
||||
import { UserPlan } from '@/types/user';
|
||||
import { getUserPlan } from '@/utils/access';
|
||||
import { normalizeToShortLang } from '@/utils/lang';
|
||||
import { DEFAULT_DAILY_TRANSLATION_QUOTA } from '@/services/constants';
|
||||
import { saveDailyUsage } from '../utils';
|
||||
|
||||
@@ -38,8 +39,8 @@ export const deeplProvider: TranslationProvider = {
|
||||
|
||||
const body = JSON.stringify({
|
||||
text: text,
|
||||
source_lang: sourceLang.toUpperCase(),
|
||||
target_lang: targetLang.toUpperCase(),
|
||||
source_lang: normalizeToShortLang(sourceLang).toUpperCase(),
|
||||
target_lang: normalizeToShortLang(targetLang).toUpperCase(),
|
||||
use_cache: useCache,
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { stubTranslation as _ } from '@/utils/misc';
|
||||
import { fetch as tauriFetch } from '@tauri-apps/plugin-http';
|
||||
import { isTauriAppPlatform } from '@/services/environment';
|
||||
import { normalizeToShortLang } from '@/utils/lang';
|
||||
import { TranslationProvider } from '../types';
|
||||
|
||||
export const googleProvider: TranslationProvider = {
|
||||
@@ -20,8 +21,8 @@ export const googleProvider: TranslationProvider = {
|
||||
const url = new URL('https://translate.googleapis.com/translate_a/single');
|
||||
url.searchParams.append('client', 'gtx');
|
||||
url.searchParams.append('dt', 't');
|
||||
url.searchParams.append('sl', sourceLang.toLowerCase() || 'auto');
|
||||
url.searchParams.append('tl', targetLang.toLowerCase());
|
||||
url.searchParams.append('sl', normalizeToShortLang(sourceLang).toLowerCase() || 'auto');
|
||||
url.searchParams.append('tl', normalizeToShortLang(targetLang).toLowerCase());
|
||||
url.searchParams.append('q', line);
|
||||
|
||||
const fetch = isTauriAppPlatform() ? tauriFetch : window.fetch;
|
||||
|
||||
@@ -51,13 +51,13 @@ export class TTSController extends EventTarget {
|
||||
|
||||
async init() {
|
||||
const availableClients = [];
|
||||
if (await this.ttsEdgeClient.init()) {
|
||||
availableClients.push(this.ttsEdgeClient);
|
||||
}
|
||||
if (this.ttsNativeClient && (await this.ttsNativeClient.init())) {
|
||||
availableClients.push(this.ttsNativeClient);
|
||||
this.ttsNativeVoices = await this.ttsNativeClient.getAllVoices();
|
||||
}
|
||||
if (await this.ttsEdgeClient.init()) {
|
||||
availableClients.push(this.ttsEdgeClient);
|
||||
}
|
||||
if (await this.ttsWebClient.init()) {
|
||||
availableClients.push(this.ttsWebClient);
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ export const isCJKLang = (lang: string | null | undefined): boolean => {
|
||||
return ['zh', 'ja', 'ko'].includes(normalizedLang);
|
||||
};
|
||||
|
||||
export const langToDefaultLocale = (langCode: string): string => {
|
||||
export const normalizeToFullLang = (langCode: string): string => {
|
||||
const mapping: Record<string, string> = {
|
||||
en: 'en-US',
|
||||
fr: 'fr-FR',
|
||||
@@ -26,24 +26,31 @@ export const langToDefaultLocale = (langCode: string): string => {
|
||||
ru: 'ru-RU',
|
||||
uk: 'uk-UA',
|
||||
zh: 'zh-Hans',
|
||||
'zh-cn': 'zh-Hans',
|
||||
'zh-tw': 'zh-Hant',
|
||||
'zh-mo': 'zh-Hant',
|
||||
'zh-hans': 'zh-Hans',
|
||||
'zh-hant': 'zh-Hant',
|
||||
};
|
||||
|
||||
return mapping[langCode] || langCode;
|
||||
return mapping[langCode.toLowerCase()] || langCode;
|
||||
};
|
||||
|
||||
export const localeToLang = (locale: string): string => {
|
||||
export const normalizeToShortLang = (langCode: string): string => {
|
||||
const lang = langCode.toLowerCase();
|
||||
const mapping: Record<string, string> = {
|
||||
'zh-CN': 'zh-Hans',
|
||||
'zh-TW': 'zh-Hant',
|
||||
'zh-HK': 'zh-Hant',
|
||||
'zh-cn': 'zh-Hans',
|
||||
'zh-tw': 'zh-Hant',
|
||||
'zh-hk': 'zh-Hant',
|
||||
'zh-mo': 'zh-Hant',
|
||||
'zh-hans': 'zh-Hans',
|
||||
'zh-hant': 'zh-Hant',
|
||||
};
|
||||
|
||||
if (locale.startsWith('zh')) {
|
||||
return mapping[locale] || 'zh-Hans';
|
||||
if (lang.startsWith('zh')) {
|
||||
return mapping[lang] || 'zh-Hans';
|
||||
}
|
||||
return mapping[locale] || locale.split('-')[0]!.toLowerCase();
|
||||
return lang.split('-')[0]!;
|
||||
};
|
||||
|
||||
export const normalizedLangCode = (lang: string | null | undefined): string => {
|
||||
|
||||
Reference in New Issue
Block a user