diff --git a/app/components/status/StatusBody.vue b/app/components/status/StatusBody.vue index 0075d4cd..67695cc7 100644 --- a/app/components/status/StatusBody.vue +++ b/app/components/status/StatusBody.vue @@ -11,7 +11,7 @@ const { withAction?: boolean }>() -const { translation } = useTranslation(status, getLanguageCode()) +const { translation } = await useTranslation(status, getLanguageCode()) const emojisObject = useEmojisFallback(() => status.emojis) const vnode = computed(() => { diff --git a/app/components/status/StatusTranslation.vue b/app/components/status/StatusTranslation.vue index fec94740..e1cc8019 100644 --- a/app/components/status/StatusTranslation.vue +++ b/app/components/status/StatusTranslation.vue @@ -9,7 +9,7 @@ const { toggle: _toggleTranslation, translation, enabled: isTranslationEnabled, -} = useTranslation(status, getLanguageCode()) +} = await useTranslation(status, getLanguageCode()) const preferenceHideTranslation = usePreferences('hideTranslation') const showButton = computed(() => diff --git a/app/composables/masto/translate.ts b/app/composables/masto/translate.ts index cd4e9cd5..d0a2920f 100644 --- a/app/composables/masto/translate.ts +++ b/app/composables/masto/translate.ts @@ -42,6 +42,10 @@ export const supportedTranslationCodes = [ 'zh', ] as const +const translationAPISupported = 'Translator' in globalThis && 'LanguageDetector' in globalThis + +const anchorMarkupRegEx = /]*>.*?<\/a>/g + export function getLanguageCode() { let code = 'en' const getCode = (code: string) => code.replace(/-.*$/, '') @@ -58,6 +62,13 @@ interface TranslationErr { } } +function replaceTranslatedLinksWithOriginal(text: string) { + return text.replace(anchorMarkupRegEx, (match) => { + const tagLink = anchorMarkupRegEx.exec(text) + return tagLink ? tagLink[0] : match + }) +} + export async function translateText(text: string, from: string | null | undefined, to: string) { const config = useRuntimeConfig() const status = ref({ @@ -65,7 +76,6 @@ export async function translateText(text: string, from: string | null | undefine error: '', text: '', }) - const regex = /]*>.*?<\/a>/g try { const response = await ($fetch as any)(config.public.translateApi, { method: 'POST', @@ -78,11 +88,7 @@ export async function translateText(text: string, from: string | null | undefine }, }) as TranslationResponse status.value.success = true - // replace the translated links with the original - status.value.text = response.translatedText.replace(regex, (match) => { - const tagLink = regex.exec(text) - return tagLink ? tagLink[0] : match - }) + status.value.text = replaceTranslatedLinksWithOriginal(response.translatedText) } catch (err) { // TODO: improve type @@ -102,17 +108,27 @@ const translations = new WeakMap() -export function useTranslation(status: mastodon.v1.Status | mastodon.v1.StatusEdit, to: string) { +export async function useTranslation(status: mastodon.v1.Status | mastodon.v1.StatusEdit, to: string) { if (!translations.has(status)) translations.set(status, reactive({ visible: false, text: '', success: false, error: '' })) const translation = translations.get(status)! const userSettings = useUserSettings() - const shouldTranslate = 'language' in status && status.language && status.language !== to - && supportedTranslationCodes.includes(to as any) - && supportedTranslationCodes.includes(status.language as any) - && !userSettings.value.disabledTranslationLanguages.includes(status.language) + let shouldTranslate = false + if ('language' in status) { + shouldTranslate = typeof status.language === 'string' && status.language !== to && !userSettings.value.disabledTranslationLanguages.includes(status.language) + if (!translationAPISupported) { + shouldTranslate = shouldTranslate && supportedTranslationCodes.includes(to as any) + && supportedTranslationCodes.includes(status.language as any) + } + else { + shouldTranslate = shouldTranslate && (await (globalThis as any).Translator.availability({ + sourceLanguage: status.language, + targetLanguage: to, + })) !== 'unavailable' + } + } const enabled = /*! !useRuntimeConfig().public.translateApi && */ shouldTranslate async function toggle() { @@ -120,12 +136,57 @@ export function useTranslation(status: mastodon.v1.Status | mastodon.v1.StatusEd return if (!translation.text) { - const translated = await translateText(status.content, status.language, to) + let translated = { + value: { + error: '', + text: '', + success: false, + }, + } + if (translationAPISupported && 'language' in status) { + let sourceLanguage = status.language + if (!sourceLanguage) { + const languageDetector = await (globalThis as any).LanguageDetector.create() + // Make sure HTML markup doesn't derail language detection. + const div = document.createElement('div') + div.innerHTML = status.content + // eslint-disable-next-line unicorn/prefer-dom-node-text-content + const detectedLanguages = await languageDetector.detect(div.innerText) + sourceLanguage = detectedLanguages[0].detectedLanguage + if (sourceLanguage === 'und') { + throw new Error('Could not detect source language.') + } + } + const translator = await (globalThis as any).Translator.create({ + sourceLanguage, + targetLanguage: to, + }) + try { + let text = await translator.translate(status.content) + text = replaceTranslatedLinksWithOriginal(text) + translated.value = { + error: '', + text, + success: true, + } + } + catch (error) { + translated.value = { + error: (error as Error).message, + text: '', + success: false, + } + } + } + else { + if ('language' in status) { + translated = await translateText(status.content, status.language, to) + } + } translation.error = translated.value.error translation.text = translated.value.text translation.success = translated.value.success } - translation.visible = !translation.visible }