mirror of
https://example.com
synced 2024-11-23 05:56:38 +09:00
feat: translate button on posts in foreign languages
This commit is contained in:
parent
9b3ddbb027
commit
dc71efe43c
9 changed files with 111 additions and 0 deletions
|
@ -10,6 +10,7 @@
|
||||||
|
|
||||||
## 主要な変更点
|
## 主要な変更点
|
||||||
|
|
||||||
|
- 投稿言語を自動検出して外国語の投稿に翻訳ボタンを表示する設定を追加
|
||||||
- モバイル表示の下部のウィジェットボタンを再読み込みボタンに変更可能に
|
- モバイル表示の下部のウィジェットボタンを再読み込みボタンに変更可能に
|
||||||
- スマートフォンでウィジェットは使わないけど再読み込みはたくさんする人はいそう
|
- スマートフォンでウィジェットは使わないけど再読み込みはたくさんする人はいそう
|
||||||
- モバイル表示の下部のチャットボタンをアカウント切り替えボタンに変更可能に
|
- モバイル表示の下部のチャットボタンをアカウント切り替えボタンに変更可能に
|
||||||
|
|
|
@ -1142,6 +1142,7 @@ deletePasskeys: "Delete passkeys"
|
||||||
delete2faConfirm: "This will irreversibly delete 2FA on this account. Proceed?"
|
delete2faConfirm: "This will irreversibly delete 2FA on this account. Proceed?"
|
||||||
deletePasskeysConfirm: "This will irreversibly delete all passkeys and security keys on this account. Proceed?"
|
deletePasskeysConfirm: "This will irreversibly delete all passkeys and security keys on this account. Proceed?"
|
||||||
inputNotMatch: "Input does not match"
|
inputNotMatch: "Input does not match"
|
||||||
|
detectPostLanguage: "Automatically detect the language and show a translate button for non-English posts"
|
||||||
|
|
||||||
_sensitiveMediaDetection:
|
_sensitiveMediaDetection:
|
||||||
description: "Reduces the effort of server moderation through automatically recognizing
|
description: "Reduces the effort of server moderation through automatically recognizing
|
||||||
|
|
|
@ -995,6 +995,7 @@ forMobile: "モバイル向け"
|
||||||
replaceChatButtonWithAccountButton: "画面下部のチャットのボタンをアカウント切り替えボタンに変更する"
|
replaceChatButtonWithAccountButton: "画面下部のチャットのボタンをアカウント切り替えボタンに変更する"
|
||||||
replaceWidgetsButtonWithReloadButton: "画面下部のウィジェットのボタンを再読み込みボタンに変更する"
|
replaceWidgetsButtonWithReloadButton: "画面下部のウィジェットのボタンを再読み込みボタンに変更する"
|
||||||
addRe: "閲覧注意の投稿への返信で、注釈の先頭に\"re:\"を追加する"
|
addRe: "閲覧注意の投稿への返信で、注釈の先頭に\"re:\"を追加する"
|
||||||
|
detectPostLanguage: "投稿の言語を自動検出し、外国語の投稿に翻訳ボタンを表示する"
|
||||||
|
|
||||||
_sensitiveMediaDetection:
|
_sensitiveMediaDetection:
|
||||||
description: "機械学習を使って自動でセンシティブなメディアを検出し、モデレーションに役立てられます。サーバーの負荷が少し増えます。"
|
description: "機械学習を使って自動でセンシティブなメディアを検出し、モデレーションに役立てられます。サーバーの負荷が少し増えます。"
|
||||||
|
|
|
@ -83,6 +83,7 @@
|
||||||
"three": "0.146.0",
|
"three": "0.146.0",
|
||||||
"throttle-debounce": "5.0.0",
|
"throttle-debounce": "5.0.0",
|
||||||
"tinycolor2": "1.6.0",
|
"tinycolor2": "1.6.0",
|
||||||
|
"tinyld": "1.3.4",
|
||||||
"tsc-alias": "1.8.7",
|
"tsc-alias": "1.8.7",
|
||||||
"tsconfig-paths": "4.2.0",
|
"tsconfig-paths": "4.2.0",
|
||||||
"twemoji-parser": "14.0.0",
|
"twemoji-parser": "14.0.0",
|
||||||
|
|
|
@ -219,6 +219,14 @@
|
||||||
<i class="ph-minus ph-bold ph-lg"></i>
|
<i class="ph-minus ph-bold ph-lg"></i>
|
||||||
</button>
|
</button>
|
||||||
<XQuoteButton class="button" :note="appearNote" />
|
<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
|
<button
|
||||||
ref="menuButton"
|
ref="menuButton"
|
||||||
v-tooltip.noDelay.bottom="i18n.ts.more"
|
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 * as mfm from "mfm-js";
|
||||||
import type { Ref } from "vue";
|
import type { Ref } from "vue";
|
||||||
import type * as misskey from "firefish-js";
|
import type * as misskey from "firefish-js";
|
||||||
|
import { detect as detectLanguage } from "tinyld";
|
||||||
import MkNoteSub from "@/components/MkNoteSub.vue";
|
import MkNoteSub from "@/components/MkNoteSub.vue";
|
||||||
import MkSubNoteContent from "./MkSubNoteContent.vue";
|
import MkSubNoteContent from "./MkSubNoteContent.vue";
|
||||||
import XNoteHeader from "@/components/MkNoteHeader.vue";
|
import XNoteHeader from "@/components/MkNoteHeader.vue";
|
||||||
|
@ -350,6 +359,35 @@ const translating = ref(false);
|
||||||
const enableEmojiReactions = defaultStore.state.enableEmojiReactions;
|
const enableEmojiReactions = defaultStore.state.enableEmojiReactions;
|
||||||
const expandOnNoteClick = defaultStore.state.expandOnNoteClick;
|
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 = {
|
const keymap = {
|
||||||
r: () => reply(true),
|
r: () => reply(true),
|
||||||
"e|a|plus": () => react(true),
|
"e|a|plus": () => react(true),
|
||||||
|
@ -914,6 +952,10 @@ defineExpose({
|
||||||
&.reacted {
|
&.reacted {
|
||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.accent {
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -124,6 +124,14 @@
|
||||||
<i class="ph-minus ph-bold ph-lg"></i>
|
<i class="ph-minus ph-bold ph-lg"></i>
|
||||||
</button>
|
</button>
|
||||||
<XQuoteButton class="button" :note="appearNote" />
|
<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
|
<button
|
||||||
ref="menuButton"
|
ref="menuButton"
|
||||||
v-tooltip.noDelay.bottom="i18n.ts.more"
|
v-tooltip.noDelay.bottom="i18n.ts.more"
|
||||||
|
@ -180,6 +188,8 @@
|
||||||
import { inject, ref } from "vue";
|
import { inject, ref } from "vue";
|
||||||
import type { Ref } from "vue";
|
import type { Ref } from "vue";
|
||||||
import * as misskey from "firefish-js";
|
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 XNoteHeader from "@/components/MkNoteHeader.vue";
|
||||||
import MkSubNoteContent from "@/components/MkSubNoteContent.vue";
|
import MkSubNoteContent from "@/components/MkSubNoteContent.vue";
|
||||||
import XReactionsViewer from "@/components/MkReactionsViewer.vue";
|
import XReactionsViewer from "@/components/MkReactionsViewer.vue";
|
||||||
|
@ -265,6 +275,35 @@ const replies: misskey.entities.Note[] =
|
||||||
const enableEmojiReactions = defaultStore.state.enableEmojiReactions;
|
const enableEmojiReactions = defaultStore.state.enableEmojiReactions;
|
||||||
const expandOnNoteClick = defaultStore.state.expandOnNoteClick;
|
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({
|
useNoteCapture({
|
||||||
rootEl: el,
|
rootEl: el,
|
||||||
note: $$(appearNote),
|
note: $$(appearNote),
|
||||||
|
@ -522,6 +561,10 @@ function noteClick(e) {
|
||||||
&.reacted {
|
&.reacted {
|
||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.accent {
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -77,6 +77,12 @@
|
||||||
{{ i18n.ts.reflectMayTakeTime }}</template
|
{{ i18n.ts.reflectMayTakeTime }}</template
|
||||||
></FormSwitch
|
></FormSwitch
|
||||||
>
|
>
|
||||||
|
<FormSwitch v-model="detectPostLanguage" class="_formBlock"
|
||||||
|
>{{ i18n.ts.detectPostLanguage
|
||||||
|
}}<span class="_beta">{{
|
||||||
|
i18n.ts.originalFeature
|
||||||
|
}}</span></FormSwitch
|
||||||
|
>
|
||||||
<FormSwitch v-model="addRe" class="_formBlock"
|
<FormSwitch v-model="addRe" class="_formBlock"
|
||||||
>{{ i18n.ts.addRe
|
>{{ i18n.ts.addRe
|
||||||
}}<span class="_beta">{{
|
}}<span class="_beta">{{
|
||||||
|
@ -412,6 +418,9 @@ const replaceWidgetsButtonWithReloadButton = computed(
|
||||||
defaultStore.makeGetterSetter("replaceWidgetsButtonWithReloadButton"),
|
defaultStore.makeGetterSetter("replaceWidgetsButtonWithReloadButton"),
|
||||||
);
|
);
|
||||||
const addRe = computed(defaultStore.makeGetterSetter("addRe"));
|
const addRe = computed(defaultStore.makeGetterSetter("addRe"));
|
||||||
|
const detectPostLanguage = computed(
|
||||||
|
defaultStore.makeGetterSetter("detectPostLanguage"),
|
||||||
|
);
|
||||||
|
|
||||||
watch(swipeOnDesktop, () => {
|
watch(swipeOnDesktop, () => {
|
||||||
defaultStore.set("swipeOnMobile", true);
|
defaultStore.set("swipeOnMobile", true);
|
||||||
|
|
|
@ -362,6 +362,10 @@ export const defaultStore = markRaw(
|
||||||
where: "account",
|
where: "account",
|
||||||
default: true,
|
default: true,
|
||||||
},
|
},
|
||||||
|
detectPostLanguage: {
|
||||||
|
where: "deviceAccount",
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -842,6 +842,9 @@ importers:
|
||||||
tinycolor2:
|
tinycolor2:
|
||||||
specifier: 1.6.0
|
specifier: 1.6.0
|
||||||
version: 1.6.0
|
version: 1.6.0
|
||||||
|
tinyld:
|
||||||
|
specifier: 1.3.4
|
||||||
|
version: 1.3.4
|
||||||
tsc-alias:
|
tsc-alias:
|
||||||
specifier: 1.8.7
|
specifier: 1.8.7
|
||||||
version: 1.8.7
|
version: 1.8.7
|
||||||
|
@ -17582,6 +17585,12 @@ packages:
|
||||||
resolution: {integrity: sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==}
|
resolution: {integrity: sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==}
|
||||||
dev: true
|
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:
|
/titleize@3.0.0:
|
||||||
resolution: {integrity: sha512-KxVu8EYHDPBdUYdKZdKtU2aj2XfEx9AfjXxE/Aj0vT06w2icA09Vus1rh6eSu1y01akYg6BjIK/hxyLJINoMLQ==}
|
resolution: {integrity: sha512-KxVu8EYHDPBdUYdKZdKtU2aj2XfEx9AfjXxE/Aj0vT06w2icA09Vus1rh6eSu1y01akYg6BjIK/hxyLJINoMLQ==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
|
|
Loading…
Reference in a new issue