language for post translation & translate button
This commit is contained in:
parent
26b4c6404a
commit
ba3bf35167
9 changed files with 185 additions and 6 deletions
|
@ -1139,6 +1139,8 @@ confirm: "Confirm"
|
|||
importZip: "Import ZIP"
|
||||
exportZip: "Export ZIP"
|
||||
emojiPackCreator: "Emoji pack creator"
|
||||
languageForTranslation: "Post translation language"
|
||||
detectPostLanguage: "Automatically detect the language and show a translate button for posts in foreign languages"
|
||||
|
||||
_sensitiveMediaDetection:
|
||||
description: "Reduces the effort of server moderation through automatically recognizing
|
||||
|
|
|
@ -988,6 +988,8 @@ youHaveUnreadAnnouncements: "未読のお知らせがあります"
|
|||
neverShow: "今後表示しない"
|
||||
remindMeLater: "また後で"
|
||||
addRe: "閲覧注意の投稿への返信で、注釈の先頭に\"re:\"を追加する"
|
||||
languageForTranslation: "投稿翻訳に使用する言語"
|
||||
detectPostLanguage: "投稿の言語を自動検出し、外国語の投稿に翻訳ボタンを表示する"
|
||||
|
||||
_sensitiveMediaDetection:
|
||||
description: "機械学習を使って自動でセンシティブなメディアを検出し、モデレーションに役立てられます。サーバーの負荷が少し増えます。"
|
||||
|
|
|
@ -81,6 +81,7 @@
|
|||
"three": "0.156.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",
|
||||
|
|
|
@ -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"
|
||||
@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, 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 MkSubNoteContent from "./MkSubNoteContent.vue";
|
||||
import MkNoteSub from "@/components/MkNoteSub.vue";
|
||||
import XNoteHeader from "@/components/MkNoteHeader.vue";
|
||||
|
@ -346,6 +355,57 @@ const translation = ref(null);
|
|||
const translating = ref(false);
|
||||
const enableEmojiReactions = defaultStore.state.enableEmojiReactions;
|
||||
const expandOnNoteClick = defaultStore.state.expandOnNoteClick;
|
||||
const lang = localStorage.getItem("lang");
|
||||
const translateLang = localStorage.getItem("translateLang");
|
||||
|
||||
function detectLanguage(text: string) {
|
||||
const nodes = mfm.parse(text);
|
||||
const filtered = mfm.extract(nodes, (node) => {
|
||||
return node.type === "text" || node.type === "quote";
|
||||
});
|
||||
const purified = mfm.toString(filtered);
|
||||
return detectLanguage_(purified);
|
||||
}
|
||||
|
||||
const isForeignLanguage: boolean =
|
||||
defaultStore.state.detectPostLanguage &&
|
||||
appearNote.value.text != null &&
|
||||
(() => {
|
||||
const targetLang = (translateLang || lang || navigator.language)?.slice(
|
||||
0,
|
||||
2,
|
||||
);
|
||||
const postLang = detectLanguage(appearNote.value.text);
|
||||
return postLang !== "" && postLang !== targetLang;
|
||||
})();
|
||||
|
||||
async function translate_(noteId: number, targetLang: string) {
|
||||
return await os.api("notes/translate", {
|
||||
noteId: noteId,
|
||||
targetLang: targetLang,
|
||||
});
|
||||
}
|
||||
|
||||
async function translate() {
|
||||
if (translation.value != null) return;
|
||||
translating.value = true;
|
||||
translation.value = await translate_(
|
||||
appearNote.value.id,
|
||||
translateLang || lang || navigator.language,
|
||||
);
|
||||
|
||||
// use UI language as the second translation language
|
||||
if (
|
||||
translateLang != null &&
|
||||
lang != null &&
|
||||
translateLang !== lang &&
|
||||
(!translation.value ||
|
||||
translation.value.sourceLang.toLowerCase() ===
|
||||
translateLang.slice(0, 2))
|
||||
)
|
||||
translation.value = await translate_(appearNote.value.id, lang);
|
||||
translating.value = false;
|
||||
}
|
||||
|
||||
const keymap = {
|
||||
r: () => reply(true),
|
||||
|
|
|
@ -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"
|
||||
@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 { computed, inject, ref } from "vue";
|
||||
import type { Ref } from "vue";
|
||||
import type * as misskey from "firefish-js";
|
||||
import type * 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";
|
||||
|
@ -266,6 +276,57 @@ const replies: misskey.entities.Note[] =
|
|||
.reverse() ?? [];
|
||||
const enableEmojiReactions = defaultStore.state.enableEmojiReactions;
|
||||
const expandOnNoteClick = defaultStore.state.expandOnNoteClick;
|
||||
const lang = localStorage.getItem("lang");
|
||||
const translateLang = localStorage.getItem("translateLang");
|
||||
|
||||
function detectLanguage(text: string) {
|
||||
const nodes = mfm.parse(text);
|
||||
const filtered = mfm.extract(nodes, (node) => {
|
||||
return node.type === "text" || node.type === "quote";
|
||||
});
|
||||
const purified = mfm.toString(filtered);
|
||||
return detectLanguage_(purified);
|
||||
}
|
||||
|
||||
const isForeignLanguage: boolean =
|
||||
defaultStore.state.detectPostLanguage &&
|
||||
appearNote.value.text != null &&
|
||||
(() => {
|
||||
const targetLang = (translateLang || lang || navigator.language)?.slice(
|
||||
0,
|
||||
2,
|
||||
);
|
||||
const postLang = detectLanguage(appearNote.value.text);
|
||||
return postLang !== "" && postLang !== targetLang;
|
||||
})();
|
||||
|
||||
async function translate_(noteId: number, targetLang: string) {
|
||||
return await os.api("notes/translate", {
|
||||
noteId: noteId,
|
||||
targetLang: targetLang,
|
||||
});
|
||||
}
|
||||
|
||||
async function translate() {
|
||||
if (translation.value != null) return;
|
||||
translating.value = true;
|
||||
translation.value = await translate_(
|
||||
appearNote.value.id,
|
||||
translateLang || lang || navigator.language,
|
||||
);
|
||||
|
||||
// use UI language as the second translation language
|
||||
if (
|
||||
translateLang != null &&
|
||||
lang != null &&
|
||||
translateLang !== lang &&
|
||||
(!translation.value ||
|
||||
translation.value.sourceLang.toLowerCase() ===
|
||||
translateLang.slice(0, 2))
|
||||
)
|
||||
translation.value = await translate_(appearNote.value.id, lang);
|
||||
translating.value = false;
|
||||
}
|
||||
|
||||
useNoteCapture({
|
||||
rootEl: el,
|
||||
|
|
|
@ -17,6 +17,15 @@
|
|||
</template>
|
||||
</FormSelect>
|
||||
|
||||
<FormSelect v-model="translateLang" class="_formBlock">
|
||||
<template #label>
|
||||
{{ i18n.ts.languageForTranslation }}
|
||||
</template>
|
||||
<option v-for="x in langs" :key="x[0]" :value="x[0]">
|
||||
{{ x[1] }}
|
||||
</option>
|
||||
</FormSelect>
|
||||
|
||||
<FormRadios v-model="overridedDeviceKind" class="_formBlock">
|
||||
<template #label>{{ i18n.ts.overridedDeviceKind }}</template>
|
||||
<option :value="null">{{ i18n.ts.auto }}</option>
|
||||
|
@ -71,6 +80,9 @@
|
|||
{{ i18n.ts.reflectMayTakeTime }}</template
|
||||
></FormSwitch
|
||||
>
|
||||
<FormSwitch v-model="detectPostLanguage" class="_formBlock">{{
|
||||
i18n.ts.detectPostLanguage
|
||||
}}</FormSwitch>
|
||||
|
||||
<FormSelect v-model="serverDisconnectedBehavior" class="_formBlock">
|
||||
<template #label>{{ i18n.ts.whenServerDisconnected }}</template>
|
||||
|
@ -266,6 +278,7 @@ import { definePageMetadata } from "@/scripts/page-metadata";
|
|||
import { deviceKind } from "@/scripts/device-kind";
|
||||
|
||||
const lang = ref(localStorage.getItem("lang"));
|
||||
const translateLang = ref(localStorage.getItem("translateLang"));
|
||||
const fontSize = ref(localStorage.getItem("fontSize"));
|
||||
const useSystemFont = ref(localStorage.getItem("useSystemFont") != null);
|
||||
|
||||
|
@ -357,6 +370,9 @@ const showAdminUpdates = computed(
|
|||
const showTimelineReplies = computed(
|
||||
defaultStore.makeGetterSetter("showTimelineReplies"),
|
||||
);
|
||||
const detectPostLanguage = computed(
|
||||
defaultStore.makeGetterSetter("detectPostLanguage"),
|
||||
);
|
||||
|
||||
watch(swipeOnDesktop, () => {
|
||||
defaultStore.set("swipeOnMobile", true);
|
||||
|
@ -367,6 +383,10 @@ watch(lang, () => {
|
|||
localStorage.removeItem("locale");
|
||||
});
|
||||
|
||||
watch(translateLang, () => {
|
||||
localStorage.setItem("translateLang", translateLang.value as string);
|
||||
});
|
||||
|
||||
watch(fontSize, () => {
|
||||
if (fontSize.value == null) {
|
||||
localStorage.removeItem("fontSize");
|
||||
|
@ -386,6 +406,7 @@ watch(useSystemFont, () => {
|
|||
watch(
|
||||
[
|
||||
lang,
|
||||
translateLang,
|
||||
fontSize,
|
||||
useSystemFont,
|
||||
enableInfiniteScroll,
|
||||
|
|
|
@ -237,15 +237,35 @@ export function getNoteMenu(props: {
|
|||
});
|
||||
}
|
||||
|
||||
async function translate_(noteId: number, targetLang: string) {
|
||||
return await os.api("notes/translate", {
|
||||
noteId: noteId,
|
||||
targetLang: targetLang,
|
||||
});
|
||||
}
|
||||
|
||||
async function translate(): Promise<void> {
|
||||
const translateLang = localStorage.getItem("translateLang");
|
||||
const lang = localStorage.getItem("lang");
|
||||
|
||||
if (props.translation.value != null) return;
|
||||
props.translating.value = true;
|
||||
const res = await os.api("notes/translate", {
|
||||
noteId: appearNote.id,
|
||||
targetLang: localStorage.getItem("lang") || navigator.language,
|
||||
});
|
||||
props.translation.value = await translate_(
|
||||
appearNote.id,
|
||||
translateLang || lang || navigator.language,
|
||||
);
|
||||
|
||||
// use UI language as the second translation target
|
||||
if (
|
||||
translateLang != null &&
|
||||
lang != null &&
|
||||
translateLang !== lang &&
|
||||
(!props.translation.value ||
|
||||
props.translation.value.sourceLang.toLowerCase() ===
|
||||
translateLang.slice(0, 2))
|
||||
)
|
||||
props.translation.value = await translate_(appearNote.id, lang);
|
||||
props.translating.value = false;
|
||||
props.translation.value = res;
|
||||
}
|
||||
|
||||
let menu;
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import { markRaw, ref } from "vue";
|
||||
import { Storage } from "./pizzax";
|
||||
import { Theme } from "./scripts/theme";
|
||||
|
||||
export const postFormActions = [];
|
||||
export const userActions = [];
|
||||
|
@ -346,6 +345,10 @@ export const defaultStore = markRaw(
|
|||
where: "account",
|
||||
default: true,
|
||||
},
|
||||
detectPostLanguage: {
|
||||
where: "deviceAccount",
|
||||
default: true,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
|
|
|
@ -830,6 +830,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
|
||||
|
@ -18042,6 +18045,12 @@ packages:
|
|||
/tinycolor2@1.6.0:
|
||||
resolution: {integrity: sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==}
|
||||
|
||||
/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'}
|
||||
|
|
Loading…
Reference in a new issue