+
{aboutNodes}
+ {needTranslation && (
+
+ {translating ? (
+
{t('Translating...')}
+ ) : translatedAbout === null ? (
+
+ ) : (
+
+ )}
+
+ )}
+
+ )
}
diff --git a/src/components/TranslateButton/index.tsx b/src/components/TranslateButton/index.tsx
index b116507b..04dcd72d 100644
--- a/src/components/TranslateButton/index.tsx
+++ b/src/components/TranslateButton/index.tsx
@@ -1,19 +1,9 @@
-import {
- EMAIL_REGEX,
- EMBEDDED_EVENT_REGEX,
- EMBEDDED_MENTION_REGEX,
- EMOJI_REGEX,
- HASHTAG_REGEX,
- URL_REGEX,
- WS_URL_REGEX
-} from '@/constants'
import { useTranslatedEvent } from '@/hooks'
import { isSupportedKind } from '@/lib/event'
import { toTranslation } from '@/lib/link'
-import { cn } from '@/lib/utils'
+import { cn, detectLanguage } from '@/lib/utils'
import { useSecondaryPage } from '@/PageManager'
import { useTranslationService } from '@/providers/TranslationServiceProvider'
-import { franc } from 'franc-min'
import { Languages, Loader } from 'lucide-react'
import { Event } from 'nostr-tools'
import { useMemo, useState } from 'react'
@@ -29,68 +19,16 @@ export default function TranslateButton({
}) {
const { i18n } = useTranslation()
const { push } = useSecondaryPage()
- const { translate, showOriginalEvent } = useTranslationService()
+ const { translateEvent, showOriginalEvent } = useTranslationService()
const [translating, setTranslating] = useState(false)
const translatedEvent = useTranslatedEvent(event.id)
const supported = useMemo(() => isSupportedKind(event.kind), [event])
const needTranslation = useMemo(() => {
- const cleanText = event.content
- .replace(URL_REGEX, '')
- .replace(WS_URL_REGEX, '')
- .replace(EMAIL_REGEX, '')
- .replace(EMBEDDED_MENTION_REGEX, '')
- .replace(EMBEDDED_EVENT_REGEX, '')
- .replace(HASHTAG_REGEX, '')
- .replace(EMOJI_REGEX, '')
- .trim()
-
- if (!cleanText) {
- return false
- }
-
- if (/[\u3040-\u309f\u30a0-\u30ff]/.test(cleanText)) {
- return i18n.language !== 'ja'
- }
- if (/[\u0e00-\u0e7f]/.test(cleanText)) {
- return i18n.language !== 'th'
- }
- if (/[\u4e00-\u9fff]/.test(cleanText)) {
- return i18n.language !== 'zh'
- }
- if (/[\u0600-\u06ff]/.test(cleanText)) {
- return i18n.language !== 'ar'
- }
- if (/[\u0400-\u04ff]/.test(cleanText)) {
- return i18n.language !== 'ru'
- }
-
- try {
- const detectedLang = franc(cleanText)
- const langMap: { [key: string]: string } = {
- ara: 'ar', // Arabic
- deu: 'de', // German
- eng: 'en', // English
- spa: 'es', // Spanish
- fra: 'fr', // French
- ita: 'it', // Italian
- jpn: 'ja', // Japanese
- pol: 'pl', // Polish
- por: 'pt', // Portuguese
- rus: 'ru', // Russian
- cmn: 'zh', // Chinese (Mandarin)
- zho: 'zh' // Chinese (alternative code)
- }
-
- const normalizedLang = langMap[detectedLang]
- if (!normalizedLang) {
- return true
- }
-
- return !i18n.language.startsWith(normalizedLang)
- } catch {
- return true
- }
+ const detected = detectLanguage(event.content)
+ if (!detected) return false
+ if (detected === 'und') return true
+ return !i18n.language.startsWith(detected)
}, [event, i18n.language])
if (!supported || !needTranslation) {
@@ -101,7 +39,7 @@ export default function TranslateButton({
if (translating) return
setTranslating(true)
- await translate(event)
+ await translateEvent(event)
.catch((error) => {
toast.error(
'Translation failed: ' + (error.message || 'An error occurred while translating the note')
diff --git a/src/i18n/locales/ar.ts b/src/i18n/locales/ar.ts
index d3e75a30..3d412b6c 100644
--- a/src/i18n/locales/ar.ts
+++ b/src/i18n/locales/ar.ts
@@ -275,6 +275,9 @@ export default {
'المستخدمون الموثوقون هم الأشخاص الذين تتابعهم والأشخاص الذين يتابعونهم.',
Continue: 'متابعة',
'Successfully updated mute list': 'تم تحديث قائمة الكتم بنجاح',
- 'No pubkeys found from {url}': 'لم يتم العثور على مفاتيح عامة من {{url}}'
+ 'No pubkeys found from {url}': 'لم يتم العثور على مفاتيح عامة من {{url}}',
+ 'Translating...': 'جارٍ الترجمة...',
+ Translate: 'ترجمة',
+ 'Show original': 'عرض الأصل'
}
}
diff --git a/src/i18n/locales/de.ts b/src/i18n/locales/de.ts
index 1b995801..f0b49545 100644
--- a/src/i18n/locales/de.ts
+++ b/src/i18n/locales/de.ts
@@ -282,6 +282,9 @@ export default {
'Vertrauenswürdige Benutzer sind Personen, denen du folgst, und Personen, denen sie folgen.',
Continue: 'Weiter',
'Successfully updated mute list': 'Stummschalteliste erfolgreich aktualisiert',
- 'No pubkeys found from {url}': 'Keine Pubkeys von {{url}} gefunden'
+ 'No pubkeys found from {url}': 'Keine Pubkeys von {{url}} gefunden',
+ 'Translating...': 'Übersetze...',
+ Translate: 'Übersetzen',
+ 'Show original': 'Original anzeigen'
}
}
diff --git a/src/i18n/locales/en.ts b/src/i18n/locales/en.ts
index ac47e229..fb5b3174 100644
--- a/src/i18n/locales/en.ts
+++ b/src/i18n/locales/en.ts
@@ -275,6 +275,9 @@ export default {
'Trusted users include people you follow and people they follow.',
Continue: 'Continue',
'Successfully updated mute list': 'Successfully updated mute list',
- 'No pubkeys found from {url}': 'No pubkeys found from {{url}}'
+ 'No pubkeys found from {url}': 'No pubkeys found from {{url}}',
+ 'Translating...': 'Translating...',
+ Translate: 'Translate',
+ 'Show original': 'Show original'
}
}
diff --git a/src/i18n/locales/es.ts b/src/i18n/locales/es.ts
index 13c42fdb..b1818c20 100644
--- a/src/i18n/locales/es.ts
+++ b/src/i18n/locales/es.ts
@@ -280,6 +280,9 @@ export default {
'Los usuarios confiables incluyen a las personas que sigues y a las personas que ellos siguen.',
Continue: 'Continuar',
'Successfully updated mute list': 'Lista de silenciamiento actualizada con éxito',
- 'No pubkeys found from {url}': 'No se encontraron pubkeys desde {{url}}'
+ 'No pubkeys found from {url}': 'No se encontraron pubkeys desde {{url}}',
+ 'Translating...': 'Traduciendo...',
+ Translate: 'Traducir',
+ 'Show original': 'Mostrar original'
}
}
diff --git a/src/i18n/locales/fr.ts b/src/i18n/locales/fr.ts
index 8515b405..55fb2810 100644
--- a/src/i18n/locales/fr.ts
+++ b/src/i18n/locales/fr.ts
@@ -280,6 +280,9 @@ export default {
'Les utilisateurs de confiance incluent les personnes que vous suivez et les personnes qu’elles suivent.',
Continue: 'Continuer',
'Successfully updated mute list': 'Liste de sourdine mise à jour avec succès',
- 'No pubkeys found from {url}': 'Aucun pubkey trouvé à partir de {{url}}'
+ 'No pubkeys found from {url}': 'Aucun pubkey trouvé à partir de {{url}}',
+ 'Translating...': 'Traduction en cours...',
+ Translate: 'Traduire',
+ 'Show original': 'Afficher l’original'
}
}
diff --git a/src/i18n/locales/it.ts b/src/i18n/locales/it.ts
index b709283f..7d4859e0 100644
--- a/src/i18n/locales/it.ts
+++ b/src/i18n/locales/it.ts
@@ -279,6 +279,9 @@ export default {
'Gli utenti fidati includono le persone che segui e le persone che seguono loro.',
Continue: 'Continua',
'Successfully updated mute list': 'Lista di silenziamento aggiornata con successo',
- 'No pubkeys found from {url}': 'Nessun pubkey trovato da {{url}}'
+ 'No pubkeys found from {url}': 'Nessun pubkey trovato da {{url}}',
+ 'Translating...': 'Traduzione in corso...',
+ Translate: 'Traduci',
+ 'Show original': 'Mostra originale'
}
}
diff --git a/src/i18n/locales/ja.ts b/src/i18n/locales/ja.ts
index e824f916..63daeec6 100644
--- a/src/i18n/locales/ja.ts
+++ b/src/i18n/locales/ja.ts
@@ -277,6 +277,9 @@ export default {
'信頼できるユーザーには、あなたがフォローしている人とその人がフォローしている人が含まれます。',
Continue: '続行',
'Successfully updated mute list': 'ミュートリストの更新に成功しました',
- 'No pubkeys found from {url}': 'URL {{url}} からのpubkeyは見つかりませんでした'
+ 'No pubkeys found from {url}': 'URL {{url}} からのpubkeyは見つかりませんでした',
+ 'Translating...': '翻訳中...',
+ Translate: '翻訳',
+ 'Show original': '原文を表示'
}
}
diff --git a/src/i18n/locales/pl.ts b/src/i18n/locales/pl.ts
index 103f911f..65160650 100644
--- a/src/i18n/locales/pl.ts
+++ b/src/i18n/locales/pl.ts
@@ -278,6 +278,9 @@ export default {
'Zaufani użytkownicy to osoby, które obserwujesz i osoby, które oni obserwują.',
Continue: 'Kontynuuj',
'Successfully updated mute list': 'Sukces aktualizacji listy zablokowanych użytkowników',
- 'No pubkeys found from {url}': 'Nie znaleziono kluczy publicznych z {{url}}'
+ 'No pubkeys found from {url}': 'Nie znaleziono kluczy publicznych z {{url}}',
+ 'Translating...': 'Tłumaczenie...',
+ Translate: 'Przetłumacz',
+ 'Show original': 'Pokaż oryginał'
}
}
diff --git a/src/i18n/locales/pt-BR.ts b/src/i18n/locales/pt-BR.ts
index ce2a6068..9b94b729 100644
--- a/src/i18n/locales/pt-BR.ts
+++ b/src/i18n/locales/pt-BR.ts
@@ -278,6 +278,9 @@ export default {
'Usuários confiáveis incluem pessoas que você segue e pessoas que elas seguem.',
Continue: 'Continuar',
'Successfully updated mute list': 'Lista de silenciados atualizada com sucesso',
- 'No pubkeys found from {url}': 'Nenhum pubkey encontrado em {{url}}'
+ 'No pubkeys found from {url}': 'Nenhum pubkey encontrado em {{url}}',
+ 'Translating...': 'Traduzindo...',
+ Translate: 'Traduzir',
+ 'Show original': 'Mostrar original'
}
}
diff --git a/src/i18n/locales/pt-PT.ts b/src/i18n/locales/pt-PT.ts
index a3246648..aad496f9 100644
--- a/src/i18n/locales/pt-PT.ts
+++ b/src/i18n/locales/pt-PT.ts
@@ -279,6 +279,9 @@ export default {
'Usuários confiáveis incluem pessoas que você segue e pessoas que elas seguem.',
Continue: 'Continuar',
'Successfully updated mute list': 'Lista de silenciados atualizada com sucesso',
- 'No pubkeys found from {url}': 'Nenhum pubkey encontrado em {{url}}'
+ 'No pubkeys found from {url}': 'Nenhum pubkey encontrado em {{url}}',
+ 'Translating...': 'Traduzindo...',
+ Translate: 'Traduzir',
+ 'Show original': 'Mostrar original'
}
}
diff --git a/src/i18n/locales/ru.ts b/src/i18n/locales/ru.ts
index f8bb4420..5b1cb5cf 100644
--- a/src/i18n/locales/ru.ts
+++ b/src/i18n/locales/ru.ts
@@ -280,6 +280,9 @@ export default {
'Доверенные пользователи включают людей, на которых вы подписаны, и людей, на которых они подписаны.',
Continue: 'Продолжить',
'Successfully updated mute list': 'Успешно обновлен список заглушенных пользователей',
- 'No pubkeys found from {url}': 'Не найдено pubkeys из {{url}}'
+ 'No pubkeys found from {url}': 'Не найдено pubkeys из {{url}}',
+ 'Translating...': 'Перевод...',
+ Translate: 'Перевести',
+ 'Show original': 'Показать оригинал'
}
}
diff --git a/src/i18n/locales/th.ts b/src/i18n/locales/th.ts
index daa46522..8ec7dd7d 100644
--- a/src/i18n/locales/th.ts
+++ b/src/i18n/locales/th.ts
@@ -274,6 +274,9 @@ export default {
'ผู้ใช้ที่เชื่อถือได้รวมถึงผู้ที่คุณติดตามและผู้ที่พวกเขาติดตาม',
Continue: 'ดำเนินการต่อ',
'Successfully updated mute list': 'อัปเดตรายการปิดเสียงสำเร็จ',
- 'No pubkeys found from {url}': 'ไม่พบ pubkeys จาก {{url}}'
+ 'No pubkeys found from {url}': 'ไม่พบ pubkeys จาก {{url}}',
+ 'Translating...': 'กำลังแปล...',
+ Translate: 'แปล',
+ 'Show original': 'แสดงต้นฉบับ'
}
}
diff --git a/src/i18n/locales/zh.ts b/src/i18n/locales/zh.ts
index 49043237..1b023e33 100644
--- a/src/i18n/locales/zh.ts
+++ b/src/i18n/locales/zh.ts
@@ -275,6 +275,9 @@ export default {
'受信任的用户包括您关注的人和他们关注的人。',
Continue: '继续',
'Successfully updated mute list': '成功更新屏蔽列表',
- 'No pubkeys found from {url}': '在 {{url}} 中未找到 pubkeys'
+ 'No pubkeys found from {url}': '在 {{url}} 中未找到 pubkeys',
+ 'Translating...': '翻译中...',
+ Translate: '翻译',
+ 'Show original': '显示原文'
}
}
diff --git a/src/lib/utils.ts b/src/lib/utils.ts
index a91496e8..b1670d40 100644
--- a/src/lib/utils.ts
+++ b/src/lib/utils.ts
@@ -1,4 +1,14 @@
+import {
+ EMAIL_REGEX,
+ EMBEDDED_EVENT_REGEX,
+ EMBEDDED_MENTION_REGEX,
+ EMOJI_REGEX,
+ HASHTAG_REGEX,
+ URL_REGEX,
+ WS_URL_REGEX
+} from '@/constants'
import { clsx, type ClassValue } from 'clsx'
+import { franc } from 'franc-min'
import { twMerge } from 'tailwind-merge'
export function cn(...inputs: ClassValue[]) {
@@ -38,3 +48,65 @@ export function isInViewport(el: HTMLElement) {
rect.right <= (window.innerWidth || document.documentElement.clientWidth)
)
}
+
+export function detectLanguage(text?: string): string | null {
+ if (!text) {
+ return null
+ }
+ const cleanText = text
+ .replace(URL_REGEX, '')
+ .replace(WS_URL_REGEX, '')
+ .replace(EMAIL_REGEX, '')
+ .replace(EMBEDDED_MENTION_REGEX, '')
+ .replace(EMBEDDED_EVENT_REGEX, '')
+ .replace(HASHTAG_REGEX, '')
+ .replace(EMOJI_REGEX, '')
+ .trim()
+
+ if (!cleanText) {
+ return null
+ }
+
+ if (/[\u3040-\u309f\u30a0-\u30ff]/.test(cleanText)) {
+ return 'ja'
+ }
+ if (/[\u0e00-\u0e7f]/.test(cleanText)) {
+ return 'th'
+ }
+ if (/[\u4e00-\u9fff]/.test(cleanText)) {
+ return 'zh'
+ }
+ if (/[\u0600-\u06ff]/.test(cleanText)) {
+ return 'ar'
+ }
+ if (/[\u0400-\u04ff]/.test(cleanText)) {
+ return 'ru'
+ }
+
+ try {
+ const detectedLang = franc(cleanText)
+ const langMap: { [key: string]: string } = {
+ ara: 'ar', // Arabic
+ deu: 'de', // German
+ eng: 'en', // English
+ spa: 'es', // Spanish
+ fra: 'fr', // French
+ ita: 'it', // Italian
+ jpn: 'ja', // Japanese
+ pol: 'pl', // Polish
+ por: 'pt', // Portuguese
+ rus: 'ru', // Russian
+ cmn: 'zh', // Chinese (Mandarin)
+ zho: 'zh' // Chinese (alternative code)
+ }
+
+ const normalizedLang = langMap[detectedLang]
+ if (!normalizedLang) {
+ return 'und'
+ }
+
+ return normalizedLang
+ } catch {
+ return 'und'
+ }
+}
diff --git a/src/providers/TranslationServiceProvider.tsx b/src/providers/TranslationServiceProvider.tsx
index 4b4dfa54..b716d894 100644
--- a/src/providers/TranslationServiceProvider.tsx
+++ b/src/providers/TranslationServiceProvider.tsx
@@ -7,12 +7,14 @@ import { createContext, useContext, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useNostr } from './NostrProvider'
-const translatedEventCache: Record