feat: translate button on posts in foreign languages

This commit is contained in:
naskya 2023-08-01 01:54:55 +09:00
parent 9b3ddbb027
commit dc71efe43c
Signed by: naskya
GPG key ID: 164DFF24E2D40139
9 changed files with 111 additions and 0 deletions

View file

@ -10,6 +10,7 @@
## 主要な変更点
- 投稿言語を自動検出して外国語の投稿に翻訳ボタンを表示する設定を追加
- モバイル表示の下部のウィジェットボタンを再読み込みボタンに変更可能に
- スマートフォンでウィジェットは使わないけど再読み込みはたくさんする人はいそう
- モバイル表示の下部のチャットボタンをアカウント切り替えボタンに変更可能に

View file

@ -1142,6 +1142,7 @@ deletePasskeys: "Delete passkeys"
delete2faConfirm: "This will irreversibly delete 2FA on this account. Proceed?"
deletePasskeysConfirm: "This will irreversibly delete all passkeys and security keys on this account. Proceed?"
inputNotMatch: "Input does not match"
detectPostLanguage: "Automatically detect the language and show a translate button for non-English posts"
_sensitiveMediaDetection:
description: "Reduces the effort of server moderation through automatically recognizing

View file

@ -995,6 +995,7 @@ forMobile: "モバイル向け"
replaceChatButtonWithAccountButton: "画面下部のチャットのボタンをアカウント切り替えボタンに変更する"
replaceWidgetsButtonWithReloadButton: "画面下部のウィジェットのボタンを再読み込みボタンに変更する"
addRe: "閲覧注意の投稿への返信で、注釈の先頭に\"re:\"を追加する"
detectPostLanguage: "投稿の言語を自動検出し、外国語の投稿に翻訳ボタンを表示する"
_sensitiveMediaDetection:
description: "機械学習を使って自動でセンシティブなメディアを検出し、モデレーションに役立てられます。サーバーの負荷が少し増えます。"

View file

@ -83,6 +83,7 @@
"three": "0.146.0",
"throttle-debounce": "5.0.0",
"tinycolor2": "1.6.0",
"tinyld": "1.3.4",
"tsc-alias": "1.8.7",
"tsconfig-paths": "4.2.0",
"twemoji-parser": "14.0.0",

View file

@ -219,6 +219,14 @@
<i class="ph-minus ph-bold ph-lg"></i>
</button>
<XQuoteButton class="button" :note="appearNote" />
<button
v-if="isForeignLanguage && translation == null"
class="button _button accent"
@click.stop="translate"
v-tooltip.noDelay.bottom="i18n.ts.translate"
>
<i class="ph-translate ph-bold ph-lg"></i>
</button>
<button
ref="menuButton"
v-tooltip.noDelay.bottom="i18n.ts.more"
@ -259,6 +267,7 @@ import { computed, inject, onMounted, onUnmounted, reactive, ref } from "vue";
import * as mfm from "mfm-js";
import type { Ref } from "vue";
import type * as misskey from "firefish-js";
import { detect as detectLanguage } from "tinyld";
import MkNoteSub from "@/components/MkNoteSub.vue";
import MkSubNoteContent from "./MkSubNoteContent.vue";
import XNoteHeader from "@/components/MkNoteHeader.vue";
@ -350,6 +359,35 @@ const translating = ref(false);
const enableEmojiReactions = defaultStore.state.enableEmojiReactions;
const expandOnNoteClick = defaultStore.state.expandOnNoteClick;
const purifyMFM = (src) => {
const nodes = mfm.parse(src);
const filtered = mfm.extract(nodes, (node) => {
return ["text", "bold", "center", "small", "italic", "strike"].includes(
node.type,
);
});
return mfm.toString(filtered);
};
const isForeignLanguage = (() => {
if (!defaultStore.state.detectPostLanguage || !appearNote.text)
return false;
const text = purifyMFM(appearNote.text).trim();
if (!text) return false;
const uiLanguage = (
localStorage.getItem("lang") || navigator.language
).slice(0, 2);
return detectLanguage(text) !== uiLanguage;
})();
const translate = async () => {
if (translation.value != null) return;
translating.value = true;
translation.value = await os.api("notes/translate", {
noteId: appearNote.id,
targetLang: localStorage.getItem("lang") || navigator.language,
});
translating.value = false;
};
const keymap = {
r: () => reply(true),
"e|a|plus": () => react(true),
@ -914,6 +952,10 @@ defineExpose({
&.reacted {
color: var(--accent);
}
&.accent {
color: var(--accent);
}
}
}
}

View file

@ -124,6 +124,14 @@
<i class="ph-minus ph-bold ph-lg"></i>
</button>
<XQuoteButton class="button" :note="appearNote" />
<button
v-if="isForeignLanguage && translation == null"
class="button _button accent"
@click.stop="translate"
v-tooltip.noDelay.bottom="i18n.ts.translate"
>
<i class="ph-translate ph-bold ph-lg"></i>
</button>
<button
ref="menuButton"
v-tooltip.noDelay.bottom="i18n.ts.more"
@ -180,6 +188,8 @@
import { inject, ref } from "vue";
import type { Ref } from "vue";
import * as misskey from "firefish-js";
import * as mfm from "mfm-js";
import { detect as detectLanguage } from "tinyld";
import XNoteHeader from "@/components/MkNoteHeader.vue";
import MkSubNoteContent from "@/components/MkSubNoteContent.vue";
import XReactionsViewer from "@/components/MkReactionsViewer.vue";
@ -265,6 +275,35 @@ const replies: misskey.entities.Note[] =
const enableEmojiReactions = defaultStore.state.enableEmojiReactions;
const expandOnNoteClick = defaultStore.state.expandOnNoteClick;
const purifyMFM = (src) => {
const nodes = mfm.parse(src);
const filtered = mfm.extract(nodes, (node) => {
return ["text", "bold", "center", "small", "italic", "strike"].includes(
node.type,
);
});
return mfm.toString(filtered);
};
const isForeignLanguage = (() => {
if (!defaultStore.state.detectPostLanguage || !appearNote.text)
return false;
const text = purifyMFM(appearNote.text).trim();
if (!text) return false;
const uiLanguage = (
localStorage.getItem("lang") || navigator.language
).slice(0, 2);
return detectLanguage(text) !== uiLanguage;
})();
const translate = async () => {
if (translation.value != null) return;
translating.value = true;
translation.value = await os.api("notes/translate", {
noteId: appearNote.id,
targetLang: localStorage.getItem("lang") || navigator.language,
});
translating.value = false;
};
useNoteCapture({
rootEl: el,
note: $$(appearNote),
@ -522,6 +561,10 @@ function noteClick(e) {
&.reacted {
color: var(--accent);
}
&.accent {
color: var(--accent);
}
}
}
}

View file

@ -77,6 +77,12 @@
{{ i18n.ts.reflectMayTakeTime }}</template
></FormSwitch
>
<FormSwitch v-model="detectPostLanguage" class="_formBlock"
>{{ i18n.ts.detectPostLanguage
}}<span class="_beta">{{
i18n.ts.originalFeature
}}</span></FormSwitch
>
<FormSwitch v-model="addRe" class="_formBlock"
>{{ i18n.ts.addRe
}}<span class="_beta">{{
@ -412,6 +418,9 @@ const replaceWidgetsButtonWithReloadButton = computed(
defaultStore.makeGetterSetter("replaceWidgetsButtonWithReloadButton"),
);
const addRe = computed(defaultStore.makeGetterSetter("addRe"));
const detectPostLanguage = computed(
defaultStore.makeGetterSetter("detectPostLanguage"),
);
watch(swipeOnDesktop, () => {
defaultStore.set("swipeOnMobile", true);

View file

@ -362,6 +362,10 @@ export const defaultStore = markRaw(
where: "account",
default: true,
},
detectPostLanguage: {
where: "deviceAccount",
default: true,
},
}),
);

View file

@ -842,6 +842,9 @@ importers:
tinycolor2:
specifier: 1.6.0
version: 1.6.0
tinyld:
specifier: 1.3.4
version: 1.3.4
tsc-alias:
specifier: 1.8.7
version: 1.8.7
@ -17582,6 +17585,12 @@ packages:
resolution: {integrity: sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==}
dev: true
/tinyld@1.3.4:
resolution: {integrity: sha512-u26CNoaInA4XpDU+8s/6Cq8xHc2T5M4fXB3ICfXPokUQoLzmPgSZU02TAkFwFMJCWTjk53gtkS8pETTreZwCqw==}
engines: {node: '>= 12.10.0', npm: '>= 6.12.0', yarn: '>= 1.20.0'}
hasBin: true
dev: true
/titleize@3.0.0:
resolution: {integrity: sha512-KxVu8EYHDPBdUYdKZdKtU2aj2XfEx9AfjXxE/Aj0vT06w2icA09Vus1rh6eSu1y01akYg6BjIK/hxyLJINoMLQ==}
engines: {node: '>=12'}