This commit is contained in:
@@ -319,5 +319,10 @@
|
||||
"Are you sure to delete {{count}} selected book(s)?_other": "هل أنت متأكد من حذف {{count}} من الكتب المحددة؟",
|
||||
"Are you sure to delete the selected book?": "هل أنت متأكد من حذف الكتاب المحدد؟",
|
||||
"Deselect": "إلغاء",
|
||||
"Select All": "الكل"
|
||||
"Select All": "الكل",
|
||||
"No translation available.": "لا توجد ترجمة متاحة.",
|
||||
"Translated by {{provider}}.": "ترجم بواسطة {{provider}}.",
|
||||
"DeepL": "ديب إل",
|
||||
"Google Translate": "ترجمة جوجل",
|
||||
"Azure Translator": "مترجم أزور"
|
||||
}
|
||||
|
||||
@@ -315,5 +315,10 @@
|
||||
"Are you sure to delete {{count}} selected book(s)?_other": "Möchtest du die {{count}} ausgewählten Bücher wirklich löschen?",
|
||||
"Are you sure to delete the selected book?": "Möchtest du das ausgewählte Buch wirklich löschen?",
|
||||
"Deselect": "Aufheben",
|
||||
"Select All": "Alle"
|
||||
"Select All": "Alle",
|
||||
"No translation available.": "Keine Übersetzung verfügbar.",
|
||||
"Translated by {{provider}}.": "Übersetzt von {{provider}}.",
|
||||
"DeepL": "DeepL",
|
||||
"Google Translate": "Google Übersetzer",
|
||||
"Azure Translator": "Azure Übersetzer"
|
||||
}
|
||||
|
||||
@@ -315,5 +315,10 @@
|
||||
"Are you sure to delete {{count}} selected book(s)?_other": "Είστε σίγουροι ότι θέλετε να διαγράψετε τα {{count}} επιλεγμένα βιβλία;",
|
||||
"Are you sure to delete the selected book?": "Είστε σίγουροι ότι θέλετε να διαγράψετε το επιλεγμένο βιβλίο;",
|
||||
"Deselect": "Άρση",
|
||||
"Select All": "Όλα"
|
||||
"Select All": "Όλα",
|
||||
"No translation available.": "Δεν είναι διαθέσιμη καμία μετάφραση.",
|
||||
"Translated by {{provider}}.": "Μεταφρασμένο από {{provider}}.",
|
||||
"DeepL": "DeepL",
|
||||
"Google Translate": "Μετάφραση Google",
|
||||
"Azure Translator": "Μεταφραστής Azure"
|
||||
}
|
||||
|
||||
@@ -316,5 +316,10 @@
|
||||
"Are you sure to delete {{count}} selected book(s)?_other": "¿Seguro que quieres eliminar los {{count}} libros seleccionados?",
|
||||
"Are you sure to delete the selected book?": "¿Seguro que quieres eliminar el libro seleccionado?",
|
||||
"Deselect": "Quitar",
|
||||
"Select All": "Todo"
|
||||
"Select All": "Todo",
|
||||
"No translation available.": "No hay traducción disponible.",
|
||||
"Translated by {{provider}}.": "Traducido por {{provider}}.",
|
||||
"DeepL": "DeepL",
|
||||
"Google Translate": "Google Translate",
|
||||
"Azure Translator": "Azure Translator"
|
||||
}
|
||||
|
||||
@@ -316,5 +316,10 @@
|
||||
"Are you sure to delete {{count}} selected book(s)?_other": "Supprimer les {{count}} livres sélectionnés ?",
|
||||
"Are you sure to delete the selected book?": "Supprimer le livre sélectionné ?",
|
||||
"Deselect": "Annuler",
|
||||
"Select All": "Tous"
|
||||
"Select All": "Tous",
|
||||
"No translation available.": "Aucune traduction disponible.",
|
||||
"Translated by {{provider}}.": "Traduit par {{provider}}.",
|
||||
"DeepL": "DeepL",
|
||||
"Google Translate": "Google Traduction",
|
||||
"Azure Translator": "Traducteur Azure"
|
||||
}
|
||||
|
||||
@@ -315,5 +315,10 @@
|
||||
"Are you sure to delete {{count}} selected book(s)?_other": "क्या आप चयनित {{count}} पुस्तकें हटाना चाहते हैं?",
|
||||
"Are you sure to delete the selected book?": "क्या आप चयनित पुस्तक हटाना चाहते हैं?",
|
||||
"Deselect": "रद्द करें",
|
||||
"Select All": "सब चुनें"
|
||||
"Select All": "सब चुनें",
|
||||
"No translation available.": "कोई अनुवाद उपलब्ध नहीं है।",
|
||||
"Translated by {{provider}}.": "{{provider}} द्वारा अनुवादित।",
|
||||
"DeepL": "डीपएल",
|
||||
"Google Translate": "गूगल अनुवाद",
|
||||
"Azure Translator": "एज़्योर अनुवादक"
|
||||
}
|
||||
|
||||
@@ -314,5 +314,10 @@
|
||||
"Are you sure to delete {{count}} selected book(s)?_other": "Apakah Anda yakin ingin menghapus {{count}} buku yang dipilih?",
|
||||
"Are you sure to delete the selected book?": "Apakah Anda yakin ingin menghapus buku yang dipilih?",
|
||||
"Deselect": "Batal",
|
||||
"Select All": "Pilih Semua"
|
||||
"Select All": "Pilih Semua",
|
||||
"No translation available.": "Tidak ada terjemahan yang tersedia.",
|
||||
"Translated by {{provider}}.": "Terjemahan oleh {{provider}}.",
|
||||
"DeepL": "DeepL",
|
||||
"Google Translate": "Google Translate",
|
||||
"Azure Translator": "Azure Translator"
|
||||
}
|
||||
|
||||
@@ -316,5 +316,10 @@
|
||||
"Are you sure to delete {{count}} selected book(s)?_other": "Sei sicuro di voler eliminare {{count}} libri selezionati?",
|
||||
"Are you sure to delete the selected book?": "Sei sicuro di voler eliminare il libro selezionato?",
|
||||
"Deselect": "Annulla",
|
||||
"Select All": "Tutti"
|
||||
"Select All": "Tutti",
|
||||
"No translation available.": "Nessuna traduzione disponibile.",
|
||||
"Translated by {{provider}}.": "Tradotto da {{provider}}.",
|
||||
"DeepL": "DeepL",
|
||||
"Google Translate": "Google Translate",
|
||||
"Azure Translator": "Traduttore Azure"
|
||||
}
|
||||
|
||||
@@ -314,5 +314,10 @@
|
||||
"Are you sure to delete {{count}} selected book(s)?_other": "選択した{{count}}冊の本を削除してもよろしいですか?",
|
||||
"Are you sure to delete the selected book?": "選択した本を削除してもよろしいですか?",
|
||||
"Deselect": "解除",
|
||||
"Select All": "全選択"
|
||||
"Select All": "全選択",
|
||||
"No translation available.": "翻訳はありません。",
|
||||
"Translated by {{provider}}.": "{{provider}}によって翻訳されました。",
|
||||
"DeepL": "DeepL",
|
||||
"Google Translate": "Google翻訳",
|
||||
"Azure Translator": "Azure翻訳"
|
||||
}
|
||||
|
||||
@@ -314,5 +314,10 @@
|
||||
"Are you sure to delete {{count}} selected book(s)?_other": "선택한 {{count}}권의 책을 삭제하시겠습니까?",
|
||||
"Are you sure to delete the selected book?": "선택한 책을 삭제하시겠습니까?",
|
||||
"Deselect": "해제",
|
||||
"Select All": "전체선택"
|
||||
"Select All": "전체선택",
|
||||
"No translation available.": "번역이 없습니다.",
|
||||
"Translated by {{provider}}.": "{{provider}}에 의해 번역됨.",
|
||||
"DeepL": "DeepL",
|
||||
"Google Translate": "구글 번역",
|
||||
"Azure Translator": "Azure 번역기"
|
||||
}
|
||||
|
||||
@@ -315,5 +315,10 @@
|
||||
"Are you sure to delete {{count}} selected book(s)?_other": "Weet je zeker dat je {{count}} geselecteerde boeken wilt verwijderen?",
|
||||
"Are you sure to delete the selected book?": "Weet je zeker dat je het geselecteerde boek wilt verwijderen?",
|
||||
"Deselect": "Deselecteer",
|
||||
"Select All": "Alles"
|
||||
"Select All": "Alles",
|
||||
"No translation available.": "Geen vertaling beschikbaar.",
|
||||
"Translated by {{provider}}.": "Vertaald door {{provider}}.",
|
||||
"DeepL": "DeepL",
|
||||
"Google Translate": "Google Vertalen",
|
||||
"Azure Translator": "Azure Vertaler"
|
||||
}
|
||||
|
||||
@@ -317,5 +317,10 @@
|
||||
"Are you sure to delete {{count}} selected book(s)?_other": "Czy na pewno chcesz usunąć {{count}} wybranych książek?",
|
||||
"Are you sure to delete the selected book?": "Czy na pewno chcesz usunąć wybraną książkę?",
|
||||
"Deselect": "Odznacz",
|
||||
"Select All": "Zaznacz"
|
||||
"Select All": "Zaznacz",
|
||||
"No translation available.": "Brak dostępnego tłumaczenia.",
|
||||
"Translated by {{provider}}.": "Przetłumaczone przez {{provider}}.",
|
||||
"DeepL": "DeepL",
|
||||
"Google Translate": "Tłumacz Google",
|
||||
"Azure Translator": "Azure Translator"
|
||||
}
|
||||
|
||||
@@ -316,5 +316,10 @@
|
||||
"Are you sure to delete {{count}} selected book(s)?_other": "Tem certeza que deseja excluir {{count}} livros selecionados?",
|
||||
"Are you sure to delete the selected book?": "Tem certeza que deseja excluir o livro selecionado?",
|
||||
"Deselect": "Desmarcar",
|
||||
"Select All": "Marcar tudo"
|
||||
"Select All": "Marcar tudo",
|
||||
"No translation available.": "Sem tradução disponível.",
|
||||
"Translated by {{provider}}.": "Traduzido por {{provider}}.",
|
||||
"DeepL": "DeepL",
|
||||
"Google Translate": "Google Tradutor",
|
||||
"Azure Translator": "Tradutor Azure"
|
||||
}
|
||||
|
||||
@@ -317,5 +317,10 @@
|
||||
"Are you sure to delete {{count}} selected book(s)?_other": "Вы уверены, что хотите удалить {{count}} выбранных книг?",
|
||||
"Are you sure to delete the selected book?": "Вы уверены, что хотите удалить выбранную книгу?",
|
||||
"Deselect": "Снять",
|
||||
"Select All": "Выбрать все"
|
||||
"Select All": "Выбрать все",
|
||||
"No translation available.": "Нет доступного перевода.",
|
||||
"Translated by {{provider}}.": "Переведено {{provider}}.",
|
||||
"DeepL": "DeepL",
|
||||
"Google Translate": "Google Translate",
|
||||
"Azure Translator": "Azure Translator"
|
||||
}
|
||||
|
||||
@@ -315,5 +315,10 @@
|
||||
"Are you sure to delete {{count}} selected book(s)?_other": "{{count}} seçili kitabı silmek istediğinize emin misiniz?",
|
||||
"Are you sure to delete the selected book?": "Seçili kitabı silmek istediğinize emin misiniz?",
|
||||
"Deselect": "Kaldır",
|
||||
"Select All": "Hepsi"
|
||||
"Select All": "Hepsi",
|
||||
"No translation available.": "Çeviri mevcut değil.",
|
||||
"Translated by {{provider}}.": "{{provider}} tarafından çevrildi.",
|
||||
"DeepL": "DeepL",
|
||||
"Google Translate": "Google Çeviri",
|
||||
"Azure Translator": "Azure Çevirmen"
|
||||
}
|
||||
|
||||
@@ -317,5 +317,10 @@
|
||||
"Are you sure to delete {{count}} selected book(s)?_other": "Ви впевнені, що хочете видалити {{count}} вибраних книг?",
|
||||
"Are you sure to delete the selected book?": "Ви впевнені, що хочете видалити вибрану книгу?",
|
||||
"Deselect": "Зняти",
|
||||
"Select All": "Усі"
|
||||
"Select All": "Усі",
|
||||
"No translation available.": "Переклад недоступний.",
|
||||
"Translated by {{provider}}.": "Перекладено {{provider}}.",
|
||||
"DeepL": "DeepL",
|
||||
"Google Translate": "Google Translate",
|
||||
"Azure Translator": "Azure Translator"
|
||||
}
|
||||
|
||||
@@ -314,5 +314,10 @@
|
||||
"Are you sure to delete {{count}} selected book(s)?_other": "Bạn có chắc muốn xóa {{count}} cuốn sách đã chọn không?",
|
||||
"Are you sure to delete the selected book?": "Bạn có chắc muốn xóa cuốn sách đã chọn không?",
|
||||
"Deselect": "Bỏ chọn",
|
||||
"Select All": "Chọn tất"
|
||||
"Select All": "Chọn tất",
|
||||
"No translation available.": "Không có bản dịch nào.",
|
||||
"Translated by {{provider}}.": "Được dịch bởi {{provider}}.",
|
||||
"DeepL": "DeepL",
|
||||
"Google Translate": "Google Dịch",
|
||||
"Azure Translator": "Azure Translator"
|
||||
}
|
||||
|
||||
@@ -314,5 +314,10 @@
|
||||
"Are you sure to delete {{count}} selected book(s)?_other": "您确定要删除 {{count}} 本选中的书籍吗?",
|
||||
"Are you sure to delete the selected book?": "您确定要删除选中的书籍吗?",
|
||||
"Deselect": "取消选择",
|
||||
"Select All": "全选"
|
||||
"Select All": "全选",
|
||||
"No translation available.": "暂无可用的翻译",
|
||||
"Translated by {{provider}}.": "由 {{provider}} 提供翻译",
|
||||
"DeepL": "DeepL",
|
||||
"Google Translate": "Google Translate",
|
||||
"Azure Translator": "Azure Translator"
|
||||
}
|
||||
|
||||
@@ -314,5 +314,10 @@
|
||||
"Are you sure to delete {{count}} selected book(s)?_other": "確定要刪除已選擇的 {{count}} 本書嗎?",
|
||||
"Are you sure to delete the selected book?": "確定要刪除已選擇的書籍嗎?",
|
||||
"Deselect": "取消選取",
|
||||
"Select All": "全選"
|
||||
"Select All": "全選",
|
||||
"No translation available.": "暫無翻譯",
|
||||
"Translated by {{provider}}.": "由 {{provider}} 提供翻譯",
|
||||
"DeepL": "DeepL",
|
||||
"Google Translate": "Google Translate",
|
||||
"Azure Translator": "Azure Translator"
|
||||
}
|
||||
|
||||
@@ -61,6 +61,15 @@
|
||||
},
|
||||
{
|
||||
"url": "https://*.cloudflarestorage.com"
|
||||
},
|
||||
{
|
||||
"url": "https://edge.microsoft.com"
|
||||
},
|
||||
{
|
||||
"url": "https://*.microsofttranslator.com"
|
||||
},
|
||||
{
|
||||
"url": "https://translate.googleapis.com"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
"security": {
|
||||
"csp": {
|
||||
"default-src": "'self' 'unsafe-inline' blob: data: customprotocol: asset: http://asset.localhost ipc: http://ipc.localhost",
|
||||
"connect-src": "'self' blob: data: asset: http://asset.localhost ipc: http://ipc.localhost https://*.sentry.io https://*.posthog.com https://*.deepl.com https://*.wikipedia.org https://*.wiktionary.org https://*.supabase.co https://*.readest.com wss://speech.platform.bing.com https://*.cloudflarestorage.com",
|
||||
"connect-src": "'self' blob: data: asset: http://asset.localhost ipc: http://ipc.localhost https://*.sentry.io https://*.posthog.com https://*.deepl.com https://*.wikipedia.org https://*.wiktionary.org https://*.supabase.co https://*.readest.com wss://speech.platform.bing.com https://*.cloudflarestorage.com https://translate.googleapis.com https://*.microsofttranslator.com https://edge.microsoft.com",
|
||||
"img-src": "'self' blob: data: asset: http://asset.localhost https://*",
|
||||
"style-src": "'self' 'unsafe-inline' blob: asset: http://asset.localhost https://cdn.jsdelivr.net https://fonts.googleapis.com https://fontsapi.zeoseven.com",
|
||||
"font-src": "'self' blob: data: asset: http://asset.localhost tauri: https://db.onlinewebfonts.com https://cdn.jsdelivr.net https://fonts.gstatic.com https://fontsapi.zeoseven.com",
|
||||
|
||||
@@ -30,7 +30,7 @@ import { HIGHLIGHT_COLOR_HEX } from '@/services/constants';
|
||||
import AnnotationPopup from './AnnotationPopup';
|
||||
import WiktionaryPopup from './WiktionaryPopup';
|
||||
import WikipediaPopup from './WikipediaPopup';
|
||||
import DeepLPopup from './DeepLPopup';
|
||||
import TranslatorPopup from './TranslatorPopup';
|
||||
|
||||
const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
const _ = useTranslation();
|
||||
@@ -499,7 +499,7 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
/>
|
||||
)}
|
||||
{showDeepLPopup && trianglePosition && translatorPopupPosition && (
|
||||
<DeepLPopup
|
||||
<TranslatorPopup
|
||||
text={selection?.text as string}
|
||||
position={translatorPopupPosition}
|
||||
trianglePosition={trianglePosition}
|
||||
|
||||
+75
-15
@@ -1,3 +1,4 @@
|
||||
import clsx from 'clsx';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import Popup from '@/components/Popup';
|
||||
import { Position } from '@/utils/sel';
|
||||
@@ -6,6 +7,7 @@ import { useSettingsStore } from '@/store/settingsStore';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { useTranslator } from '@/hooks/useTranslator';
|
||||
import { TRANSLATED_LANGS } from '@/services/constants';
|
||||
import { TranslatorName } from '@/services/translators';
|
||||
|
||||
const notSupportedLangs = ['hi', 'vi'];
|
||||
|
||||
@@ -27,7 +29,7 @@ const generateTranslatorLangs = () => {
|
||||
|
||||
const TRANSLATOR_LANGS = generateTranslatorLangs();
|
||||
|
||||
interface DeepLPopupProps {
|
||||
interface TranslatorPopupProps {
|
||||
text: string;
|
||||
position: Position;
|
||||
trianglePosition: Position;
|
||||
@@ -35,7 +37,12 @@ interface DeepLPopupProps {
|
||||
popupHeight: number;
|
||||
}
|
||||
|
||||
const DeepLPopup: React.FC<DeepLPopupProps> = ({
|
||||
interface TranslatorType {
|
||||
name: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
const TranslatorPopup: React.FC<TranslatorPopupProps> = ({
|
||||
text,
|
||||
position,
|
||||
trianglePosition,
|
||||
@@ -45,6 +52,10 @@ const DeepLPopup: React.FC<DeepLPopupProps> = ({
|
||||
const _ = useTranslation();
|
||||
const { token } = useAuth();
|
||||
const { settings, setSettings } = useSettingsStore();
|
||||
const [providers, setProviders] = useState<TranslatorType[]>([]);
|
||||
const [provider, setProvider] = useState<TranslatorName>(
|
||||
settings.globalReadSettings.translationProvider as TranslatorName,
|
||||
);
|
||||
const [sourceLang, setSourceLang] = useState('AUTO');
|
||||
const [targetLang, setTargetLang] = useState(settings.globalReadSettings.translateTargetLang);
|
||||
const [translation, setTranslation] = useState<string | null>(null);
|
||||
@@ -54,7 +65,8 @@ const DeepLPopup: React.FC<DeepLPopupProps> = ({
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const { translate } = useTranslator({
|
||||
const { translate, translators } = useTranslator({
|
||||
provider,
|
||||
sourceLang,
|
||||
targetLang,
|
||||
});
|
||||
@@ -69,9 +81,25 @@ const DeepLPopup: React.FC<DeepLPopupProps> = ({
|
||||
setTargetLang(event.target.value);
|
||||
};
|
||||
|
||||
const handleProviderChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
const selectedTranslator = translators.find((t) => t.name === event.target.value);
|
||||
if (selectedTranslator) {
|
||||
settings.globalReadSettings.translationProvider = selectedTranslator.name;
|
||||
setSettings(settings);
|
||||
setProvider(selectedTranslator.name as TranslatorName);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const availableProviders = translators.map((t) => {
|
||||
return { name: t.name, label: t.label };
|
||||
});
|
||||
setProviders(availableProviders);
|
||||
}, [translators]);
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(true);
|
||||
const fetchTranslation = async () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
setTranslation(null);
|
||||
|
||||
@@ -103,7 +131,7 @@ const DeepLPopup: React.FC<DeepLPopupProps> = ({
|
||||
|
||||
fetchTranslation();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [text, token, sourceLang, targetLang]);
|
||||
}, [text, token, sourceLang, targetLang, provider]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
@@ -122,7 +150,10 @@ const DeepLPopup: React.FC<DeepLPopupProps> = ({
|
||||
<select
|
||||
value={sourceLang}
|
||||
onChange={handleSourceLangChange}
|
||||
className='select h-8 min-h-8 rounded-md border-none bg-gray-600 text-sm text-white/75 focus:outline-none focus:ring-0'
|
||||
className={clsx(
|
||||
'select h-8 min-h-8 rounded-md border-none text-end text-sm',
|
||||
'bg-gray-600 text-white/75 focus:outline-none focus:ring-0',
|
||||
)}
|
||||
>
|
||||
{[
|
||||
['AUTO', _('Auto Detect')],
|
||||
@@ -144,13 +175,16 @@ const DeepLPopup: React.FC<DeepLPopupProps> = ({
|
||||
|
||||
<div className='mx-4 flex-shrink-0 border-t border-gray-500/30'></div>
|
||||
|
||||
<div className='overflow-y-auto p-4 font-sans'>
|
||||
<div className='overflow-y-auto px-4 pt-4 font-sans'>
|
||||
<div className='mb-2 flex items-center justify-between'>
|
||||
<h2 className='text-sm font-normal'>{_('Translated Text')}</h2>
|
||||
<select
|
||||
value={targetLang}
|
||||
onChange={handleTargetLangChange}
|
||||
className='select h-8 min-h-8 rounded-md border-none bg-gray-600 text-sm text-white/75 focus:outline-none focus:ring-0'
|
||||
className={clsx(
|
||||
'select h-8 min-h-8 rounded-md border-none text-end text-sm',
|
||||
'bg-gray-600 text-white/75 focus:outline-none focus:ring-0',
|
||||
)}
|
||||
>
|
||||
{Object.entries(TRANSLATOR_LANGS)
|
||||
.sort((a, b) => a[1].localeCompare(b[1]))
|
||||
@@ -164,14 +198,40 @@ const DeepLPopup: React.FC<DeepLPopupProps> = ({
|
||||
|
||||
{loading ? (
|
||||
<p className='text-base italic text-gray-500'>{_('Loading...')}</p>
|
||||
) : error ? (
|
||||
<p className='text-base text-red-600'>{error}</p>
|
||||
) : (
|
||||
<div>
|
||||
<p className='text-base text-white/90'>
|
||||
{translation || 'No translation available.'}
|
||||
</p>
|
||||
<div className='pt-4 text-sm opacity-60'>Translated by DeepL.</div>
|
||||
{error ? (
|
||||
<p className='text-base text-red-600'>{error}</p>
|
||||
) : (
|
||||
<p className='text-base text-white/90'>
|
||||
{translation || _('No translation available.')}
|
||||
</p>
|
||||
)}
|
||||
<div className='flex h-10 items-center justify-between pt-4'>
|
||||
{provider && (
|
||||
<div className='text-xs opacity-60'>
|
||||
{error
|
||||
? ''
|
||||
: _('Translated by {{provider}}.', {
|
||||
provider: providers.find((p) => p.name === provider)?.label,
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
<select
|
||||
value={provider}
|
||||
onChange={handleProviderChange}
|
||||
className={clsx(
|
||||
'select h-8 min-h-8 rounded-md border-none text-end text-sm',
|
||||
'bg-gray-600 text-white/75 focus:outline-none focus:ring-0',
|
||||
)}
|
||||
>
|
||||
{providers.map(({ name, label }) => (
|
||||
<option key={name} value={name}>
|
||||
{label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -180,4 +240,4 @@ const DeepLPopup: React.FC<DeepLPopupProps> = ({
|
||||
);
|
||||
};
|
||||
|
||||
export default DeepLPopup;
|
||||
export default TranslatorPopup;
|
||||
@@ -1,14 +1,19 @@
|
||||
import clsx from 'clsx';
|
||||
import React, { ReactNode } from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
|
||||
interface ModalPortalProps {
|
||||
children: ReactNode;
|
||||
showOverlay?: boolean;
|
||||
}
|
||||
|
||||
const ModalPortal: React.FC<ModalPortalProps> = ({ children }) => {
|
||||
const ModalPortal: React.FC<ModalPortalProps> = ({ children, showOverlay = true }) => {
|
||||
return ReactDOM.createPortal(
|
||||
<div
|
||||
className='fixed inset-0 isolate z-50 flex items-center justify-center bg-black bg-opacity-50'
|
||||
className={clsx(
|
||||
'fixed inset-0 isolate z-50 flex items-center justify-center',
|
||||
showOverlay && 'bg-black bg-opacity-50',
|
||||
)}
|
||||
style={{ transform: 'translateZ(0)' }}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -1,23 +1,25 @@
|
||||
import { useState, useCallback, useEffect } from 'react';
|
||||
import { useAuth } from '@/context/AuthContext';
|
||||
import {
|
||||
deeplProvider,
|
||||
getFromCache,
|
||||
storeInCache,
|
||||
UseTranslatorOptions,
|
||||
} from '@/services/translators';
|
||||
import { getTranslator, getTranslators } from '@/services/translators';
|
||||
import { getFromCache, storeInCache, UseTranslatorOptions } from '@/services/translators';
|
||||
|
||||
export function useTranslator({
|
||||
provider = deeplProvider,
|
||||
provider = 'deepl',
|
||||
sourceLang = 'AUTO',
|
||||
targetLang = 'EN',
|
||||
}: UseTranslatorOptions = {}) {
|
||||
const { token } = useAuth();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [translator, setTransltor] = useState(() => getTranslator(provider));
|
||||
const [translators] = useState(() => getTranslators());
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(false);
|
||||
}, [provider.name, sourceLang, targetLang]);
|
||||
}, [provider, sourceLang, targetLang]);
|
||||
|
||||
useEffect(() => {
|
||||
setTransltor(getTranslator(provider));
|
||||
}, [provider]);
|
||||
|
||||
const translate = useCallback(
|
||||
async (
|
||||
@@ -44,7 +46,7 @@ export function useTranslator({
|
||||
text,
|
||||
sourceLanguage,
|
||||
targetLanguage,
|
||||
provider.name,
|
||||
provider,
|
||||
);
|
||||
if (cachedTranslation) return;
|
||||
|
||||
@@ -56,7 +58,7 @@ export function useTranslator({
|
||||
if (textsNeedingTranslation.length === 0) {
|
||||
const results = await Promise.all(
|
||||
textsToTranslate.map((text) =>
|
||||
getFromCache(text, sourceLanguage, targetLanguage, provider.name).then(
|
||||
getFromCache(text, sourceLanguage, targetLanguage, provider).then(
|
||||
(cached) => cached || text,
|
||||
),
|
||||
),
|
||||
@@ -68,7 +70,11 @@ export function useTranslator({
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
const translatedTexts = await provider.translate(
|
||||
const translator = translators.find((t) => t.name === provider);
|
||||
if (!translator) {
|
||||
throw new Error(`No translator found for provider: ${provider}`);
|
||||
}
|
||||
const translatedTexts = await translator.translate(
|
||||
textsNeedingTranslation,
|
||||
sourceLanguage,
|
||||
targetLanguage,
|
||||
@@ -83,7 +89,7 @@ export function useTranslator({
|
||||
translatedTexts[index] || '',
|
||||
sourceLanguage,
|
||||
targetLanguage,
|
||||
provider.name,
|
||||
provider,
|
||||
);
|
||||
}),
|
||||
);
|
||||
@@ -103,7 +109,7 @@ export function useTranslator({
|
||||
originalText,
|
||||
sourceLanguage,
|
||||
targetLanguage,
|
||||
provider.name,
|
||||
provider,
|
||||
);
|
||||
|
||||
if (cachedTranslation) {
|
||||
@@ -121,11 +127,13 @@ export function useTranslator({
|
||||
throw err instanceof Error ? err : new Error(String(err));
|
||||
}
|
||||
},
|
||||
[provider, sourceLang, targetLang, token],
|
||||
[provider, sourceLang, targetLang, translator, token],
|
||||
);
|
||||
|
||||
return {
|
||||
translate,
|
||||
translator,
|
||||
translators,
|
||||
loading,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -56,6 +56,7 @@ export const DEFAULT_READSETTINGS: ReadSettings = {
|
||||
notebookWidth: '25%',
|
||||
isNotebookPinned: false,
|
||||
autohideCursor: true,
|
||||
translationProvider: 'deepl',
|
||||
translateTargetLang: 'EN',
|
||||
|
||||
customThemes: [],
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
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';
|
||||
|
||||
interface TokenCache {
|
||||
token: string;
|
||||
expiresAt: number;
|
||||
}
|
||||
|
||||
let tokenCache: TokenCache | null = null;
|
||||
|
||||
const getAuthToken = async (): Promise<string> => {
|
||||
const now = Date.now();
|
||||
|
||||
if (tokenCache && tokenCache.expiresAt > now) {
|
||||
return tokenCache.token;
|
||||
}
|
||||
|
||||
try {
|
||||
const fetch = isTauriAppPlatform() ? tauriFetch : window.fetch;
|
||||
const tokenResponse = await fetch('https://edge.microsoft.com/translate/auth', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'User-Agent': 'Mozilla/5.0',
|
||||
},
|
||||
});
|
||||
|
||||
if (!tokenResponse.ok) {
|
||||
throw new Error(`Failed to get auth token: ${tokenResponse.status}`);
|
||||
}
|
||||
|
||||
const token = await tokenResponse.text();
|
||||
const expiresAt = now + 8 * 60 * 1000;
|
||||
|
||||
tokenCache = {
|
||||
token,
|
||||
expiresAt,
|
||||
};
|
||||
|
||||
return token;
|
||||
} catch (error) {
|
||||
console.error('Error getting Microsoft translation auth token:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const azureProvider: TranslationProvider = {
|
||||
name: 'azure',
|
||||
label: _('Azure Translator'),
|
||||
translate: async (text: string[], sourceLang: string, targetLang: string): Promise<string[]> => {
|
||||
if (!text.length) return [];
|
||||
|
||||
const results: string[] = [];
|
||||
const msSourceLang = sourceLang ? langToDefaultLocale(sourceLang.toLowerCase()) : '';
|
||||
const msTargetLang = langToDefaultLocale(targetLang.toLowerCase());
|
||||
|
||||
const translationPromises = text.map(async (line, index) => {
|
||||
if (!line?.trim().length) {
|
||||
results[index] = line;
|
||||
return;
|
||||
}
|
||||
|
||||
const url = 'https://api-edge.cognitive.microsofttranslator.com/translate';
|
||||
const params = new URLSearchParams({
|
||||
to: msTargetLang,
|
||||
'api-version': '3.0',
|
||||
});
|
||||
if (msSourceLang && msSourceLang !== 'auto') {
|
||||
params.append('from', msSourceLang);
|
||||
}
|
||||
|
||||
const token = await getAuthToken();
|
||||
const fetch = isTauriAppPlatform() ? tauriFetch : window.fetch;
|
||||
const response = await fetch(`${url}?${params.toString()}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
body: JSON.stringify([{ Text: line }]),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Translation failed with status ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (Array.isArray(data) && data.length > 0 && data[0].translations) {
|
||||
results[index] = data[0].translations[0].text || line;
|
||||
} else {
|
||||
results[index] = line;
|
||||
}
|
||||
});
|
||||
|
||||
await Promise.all(translationPromises);
|
||||
|
||||
return results;
|
||||
},
|
||||
};
|
||||
@@ -1,10 +1,12 @@
|
||||
import { getAPIBaseUrl } from '@/services/environment';
|
||||
import { stubTranslation as _ } from '@/utils/misc';
|
||||
import { TranslationProvider } from '../types';
|
||||
|
||||
const DEEPL_API_ENDPOINT = getAPIBaseUrl() + '/deepl/translate';
|
||||
|
||||
export const deeplProvider: TranslationProvider = {
|
||||
name: 'deepl',
|
||||
label: _('DeepL'),
|
||||
translate: async (
|
||||
text: string[],
|
||||
sourceLang: string,
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import { stubTranslation as _ } from '@/utils/misc';
|
||||
import { fetch as tauriFetch } from '@tauri-apps/plugin-http';
|
||||
import { isTauriAppPlatform } from '@/services/environment';
|
||||
import { TranslationProvider } from '../types';
|
||||
|
||||
export const googleProvider: TranslationProvider = {
|
||||
name: 'google',
|
||||
label: _('Google Translate'),
|
||||
translate: async (text: string[], sourceLang: string, targetLang: string): Promise<string[]> => {
|
||||
if (!text.length) return [];
|
||||
|
||||
const results: string[] = [];
|
||||
|
||||
const translationPromises = text.map(async (line, index) => {
|
||||
if (!line?.trim().length) {
|
||||
results[index] = line;
|
||||
return;
|
||||
}
|
||||
|
||||
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('q', line);
|
||||
|
||||
const fetch = isTauriAppPlatform() ? tauriFetch : window.fetch;
|
||||
const response = await fetch(url.toString());
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Translation failed with status ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
if (Array.isArray(data) && Array.isArray(data[0])) {
|
||||
const translatedText = data[0]
|
||||
.filter((segment) => Array.isArray(segment) && segment[0])
|
||||
.map((segment) => segment[0])
|
||||
.join('');
|
||||
|
||||
results[index] = translatedText || line;
|
||||
} else {
|
||||
results[index] = line;
|
||||
}
|
||||
});
|
||||
|
||||
await Promise.all(translationPromises);
|
||||
|
||||
return results;
|
||||
},
|
||||
};
|
||||
@@ -1 +1,37 @@
|
||||
export * from './deepl';
|
||||
import { TranslationProvider } from '../types';
|
||||
import { deeplProvider } from './deepl';
|
||||
import { azureProvider } from './azure';
|
||||
import { googleProvider } from './google';
|
||||
|
||||
function createTranslator<T extends string>(
|
||||
name: T,
|
||||
implementation: TranslationProvider,
|
||||
): TranslationProvider & { name: T } {
|
||||
if (name !== implementation.name) {
|
||||
throw Error(
|
||||
`Translator name "${name}" does not match implementation name "${implementation.name}"`,
|
||||
);
|
||||
}
|
||||
return { ...implementation, name };
|
||||
}
|
||||
|
||||
const deeplTranslator = createTranslator('deepl', deeplProvider);
|
||||
const azureTranslator = createTranslator('azure', azureProvider);
|
||||
const googleTranslator = createTranslator('google', googleProvider);
|
||||
|
||||
const availableTranslators = [
|
||||
deeplTranslator,
|
||||
azureTranslator,
|
||||
googleTranslator,
|
||||
// Add more translators here
|
||||
];
|
||||
|
||||
export type TranslatorName = (typeof availableTranslators)[number]['name'];
|
||||
|
||||
export const getTranslator = (name: TranslatorName): TranslationProvider | undefined => {
|
||||
return availableTranslators.find((translator) => translator.name === name);
|
||||
};
|
||||
|
||||
export const getTranslators = (): TranslationProvider[] => {
|
||||
return availableTranslators;
|
||||
};
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { TranslatorName } from './providers';
|
||||
|
||||
export interface TranslationProvider {
|
||||
name: string;
|
||||
label: string;
|
||||
translate: (
|
||||
texts: string[],
|
||||
sourceLang: string,
|
||||
@@ -14,7 +17,7 @@ export interface TranslationCache {
|
||||
}
|
||||
|
||||
export interface UseTranslatorOptions {
|
||||
provider?: TranslationProvider;
|
||||
provider?: TranslatorName;
|
||||
sourceLang?: string;
|
||||
targetLang?: string;
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ export interface ReadSettings {
|
||||
notebookWidth: string;
|
||||
isNotebookPinned: boolean;
|
||||
autohideCursor: boolean;
|
||||
translationProvider: string;
|
||||
translateTargetLang: string;
|
||||
|
||||
highlightStyle: HighlightStyle;
|
||||
|
||||
@@ -1,3 +1,28 @@
|
||||
export const isCJKStr = (str: string) => {
|
||||
return /[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Hangul}]/u.test(str ?? '');
|
||||
};
|
||||
|
||||
export const langToDefaultLocale = (langCode: string): string => {
|
||||
const mapping: Record<string, string> = {
|
||||
en: 'en-US',
|
||||
fr: 'fr-FR',
|
||||
de: 'de-DE',
|
||||
es: 'es-ES',
|
||||
it: 'it-IT',
|
||||
ja: 'ja-JP',
|
||||
ko: 'ko-KR',
|
||||
pt: 'pt-PT',
|
||||
ar: 'ar-SA',
|
||||
nl: 'nl-NL',
|
||||
pl: 'pl-PL',
|
||||
tr: 'tr-TR',
|
||||
id: 'id-ID',
|
||||
ru: 'ru-RU',
|
||||
uk: 'uk-UA',
|
||||
zh: 'zh-Hans',
|
||||
'zh-hans': 'zh-Hans',
|
||||
'zh-hant': 'zh-Hant',
|
||||
};
|
||||
|
||||
return mapping[langCode] || langCode;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user