From e484545d5effff8b62da6967b2bf549391d5d32e Mon Sep 17 00:00:00 2001
From: fly_mc <me@flymc.cc>
Date: Thu, 17 Oct 2024 23:19:17 +0800
Subject: [PATCH] add translate foreign language in MkNote

---
 packages/frontend-embed/vite.config.ts        |  7 ++++++
 packages/frontend/package.json                |  1 +
 packages/frontend/src/components/MkNote.vue   | 23 +++++++++++++++++++
 .../frontend/src/scripts/detect-language.ts   | 18 +++++++++++++++
 packages/frontend/vite.config.ts              |  7 ++++++
 5 files changed, 56 insertions(+)
 create mode 100644 packages/frontend/src/scripts/detect-language.ts

diff --git a/packages/frontend-embed/vite.config.ts b/packages/frontend-embed/vite.config.ts
index 2dbee488c5..fe134a000d 100644
--- a/packages/frontend-embed/vite.config.ts
+++ b/packages/frontend-embed/vite.config.ts
@@ -25,6 +25,13 @@ const externalPackages = [
 				: id;
 		},
 	},
+	{
+		name: 'tinyld',
+		match: /^tinyld$/,
+		path(): string {
+			return `https://cdn.jsdelivr.net/npm/tinyld@${packageInfo.dependencies.tinyld}/dist/tinyld.normal.node.mjs`
+		},
+	},
 ];
 
 const hash = (str: string, seed = 0): number => {
diff --git a/packages/frontend/package.json b/packages/frontend/package.json
index 0d9ffc0805..6f1df2fa84 100644
--- a/packages/frontend/package.json
+++ b/packages/frontend/package.json
@@ -67,6 +67,7 @@
 		"three": "0.169.0",
 		"throttle-debounce": "5.0.2",
 		"tinycolor2": "1.6.0",
+		"tinyld": "^1.3.4",
 		"tsc-alias": "1.8.10",
 		"tsconfig-paths": "4.2.0",
 		"typescript": "5.6.2",
diff --git a/packages/frontend/src/components/MkNote.vue b/packages/frontend/src/components/MkNote.vue
index 202e9d8e2d..1ba10bd684 100644
--- a/packages/frontend/src/components/MkNote.vue
+++ b/packages/frontend/src/components/MkNote.vue
@@ -80,6 +80,10 @@ SPDX-License-Identifier: AGPL-3.0-only
 							:enableEmojiMenu="true"
 							:enableEmojiMenuReaction="true"
 						/>
+						<div v-if="instance.translatorAvailable && $i && $i.policies.canUseTranslator && appearNote.text && isForeignLanguage" style="padding-top: 5px; color: var(--MI_THEME-accent);">
+						    <button v-if="!(translating || translation)" ref="translateButton" class="_button" @click.stop="translate()">{{ i18n.ts.translate }}</button>
+						    <button v-else class="_button" @click.stop="translation= null">{{ i18n.ts.close }}</button>
+					    </div>
 						<div v-if="translating || translation" :class="$style.translation">
 							<MkLoading v-if="translating" mini/>
 							<div v-else-if="translation">
@@ -215,6 +219,8 @@ import { isEnabledUrlPreview } from '@/instance.js';
 import { type Keymap } from '@/scripts/hotkey.js';
 import { focusPrev, focusNext } from '@/scripts/focus.js';
 import { getAppearNote } from '@/scripts/get-appear-note.js';
+import { miLocalStorage } from '@/local-storage.js';
+import detectLanguage from '@/scripts/detect-language.js';
 
 const props = withDefaults(defineProps<{
 	note: Misskey.entities.Note;
@@ -572,6 +578,23 @@ async function clip(): Promise<void> {
 	os.popupMenu(await getNoteClipMenu({ note: note.value, isDeleted, currentClip: currentClip?.value }), clipButton.value).then(focus);
 }
 
+const isForeignLanguage: boolean = appearNote.value.text != null && (() => {
+	const targetLang = (miLocalStorage.getItem('lang') ?? navigator.language).slice(0, 2);
+	const postLang = detectLanguage(appearNote.value.text);
+	return postLang !== '' && postLang !== targetLang;
+})();
+
+async function translate(): Promise<void> {
+	if (props.translation.value != null) return;
+	props.translating.value = true;
+	const res = await misskeyApi('notes/translate', {
+		noteId: appearNote.id,
+		targetLang: miLocalStorage.getItem('lang') ?? navigator.language,
+	});
+	props.translating.value = false;
+	props.translation.value = res;
+}
+
 function showRenoteMenu(): void {
 	if (props.mock) {
 		return;
diff --git a/packages/frontend/src/scripts/detect-language.ts b/packages/frontend/src/scripts/detect-language.ts
new file mode 100644
index 0000000000..ff40779a02
--- /dev/null
+++ b/packages/frontend/src/scripts/detect-language.ts
@@ -0,0 +1,18 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { detect } from 'tinyld';
+import * as mfm from 'mfm-js';
+
+export default function detectLanguage(text: string): string {
+	const nodes = mfm.parse(text);
+	const filtered = mfm.extract(nodes, (node) => {
+		return node.type === 'text' || node.type === 'quote';
+	});
+	const purified = mfm.toString(filtered);
+
+	if (detect(purified) === '') return 'en';
+	return detect(purified);
+}
diff --git a/packages/frontend/vite.config.ts b/packages/frontend/vite.config.ts
index 504562a91e..610b1702e5 100644
--- a/packages/frontend/vite.config.ts
+++ b/packages/frontend/vite.config.ts
@@ -27,6 +27,13 @@ const externalPackages = [
 				: id;
 		},
 	},
+	{
+		name: 'tinyld',
+		match: /^tinyld$/,
+		path(): string {
+			return `https://cdn.jsdelivr.net/npm/tinyld@${packageInfo.dependencies.tinyld}/dist/tinyld.normal.node.mjs`
+		},
+	},
 ];
 
 const hash = (str: string, seed = 0): number => {