language for post translation & translate button

This commit is contained in:
naskya 2023-09-03 20:50:15 +00:00 committed by Kainoa Kanter
parent 26b4c6404a
commit ba3bf35167
9 changed files with 185 additions and 6 deletions

View file

@ -1139,6 +1139,8 @@ confirm: "Confirm"
importZip: "Import ZIP" importZip: "Import ZIP"
exportZip: "Export ZIP" exportZip: "Export ZIP"
emojiPackCreator: "Emoji pack creator" emojiPackCreator: "Emoji pack creator"
languageForTranslation: "Post translation language"
detectPostLanguage: "Automatically detect the language and show a translate button for posts in foreign languages"
_sensitiveMediaDetection: _sensitiveMediaDetection:
description: "Reduces the effort of server moderation through automatically recognizing description: "Reduces the effort of server moderation through automatically recognizing

View file

@ -988,6 +988,8 @@ youHaveUnreadAnnouncements: "未読のお知らせがあります"
neverShow: "今後表示しない" neverShow: "今後表示しない"
remindMeLater: "また後で" remindMeLater: "また後で"
addRe: "閲覧注意の投稿への返信で、注釈の先頭に\"re:\"を追加する" addRe: "閲覧注意の投稿への返信で、注釈の先頭に\"re:\"を追加する"
languageForTranslation: "投稿翻訳に使用する言語"
detectPostLanguage: "投稿の言語を自動検出し、外国語の投稿に翻訳ボタンを表示する"
_sensitiveMediaDetection: _sensitiveMediaDetection:
description: "機械学習を使って自動でセンシティブなメディアを検出し、モデレーションに役立てられます。サーバーの負荷が少し増えます。" description: "機械学習を使って自動でセンシティブなメディアを検出し、モデレーションに役立てられます。サーバーの負荷が少し増えます。"

View file

@ -81,6 +81,7 @@
"three": "0.156.0", "three": "0.156.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",

View file

@ -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"
@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, 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 MkSubNoteContent from "./MkSubNoteContent.vue"; import MkSubNoteContent from "./MkSubNoteContent.vue";
import MkNoteSub from "@/components/MkNoteSub.vue"; import MkNoteSub from "@/components/MkNoteSub.vue";
import XNoteHeader from "@/components/MkNoteHeader.vue"; import XNoteHeader from "@/components/MkNoteHeader.vue";
@ -346,6 +355,57 @@ const translation = ref(null);
const translating = ref(false); 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 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 = { const keymap = {
r: () => reply(true), r: () => reply(true),

View file

@ -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"
@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 { computed, inject, ref } from "vue"; import { computed, inject, ref } from "vue";
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 type * 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";
@ -266,6 +276,57 @@ const replies: misskey.entities.Note[] =
.reverse() ?? []; .reverse() ?? [];
const enableEmojiReactions = defaultStore.state.enableEmojiReactions; const enableEmojiReactions = defaultStore.state.enableEmojiReactions;
const expandOnNoteClick = defaultStore.state.expandOnNoteClick; 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({ useNoteCapture({
rootEl: el, rootEl: el,

View file

@ -17,6 +17,15 @@
</template> </template>
</FormSelect> </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"> <FormRadios v-model="overridedDeviceKind" class="_formBlock">
<template #label>{{ i18n.ts.overridedDeviceKind }}</template> <template #label>{{ i18n.ts.overridedDeviceKind }}</template>
<option :value="null">{{ i18n.ts.auto }}</option> <option :value="null">{{ i18n.ts.auto }}</option>
@ -71,6 +80,9 @@
{{ i18n.ts.reflectMayTakeTime }}</template {{ i18n.ts.reflectMayTakeTime }}</template
></FormSwitch ></FormSwitch
> >
<FormSwitch v-model="detectPostLanguage" class="_formBlock">{{
i18n.ts.detectPostLanguage
}}</FormSwitch>
<FormSelect v-model="serverDisconnectedBehavior" class="_formBlock"> <FormSelect v-model="serverDisconnectedBehavior" class="_formBlock">
<template #label>{{ i18n.ts.whenServerDisconnected }}</template> <template #label>{{ i18n.ts.whenServerDisconnected }}</template>
@ -266,6 +278,7 @@ import { definePageMetadata } from "@/scripts/page-metadata";
import { deviceKind } from "@/scripts/device-kind"; import { deviceKind } from "@/scripts/device-kind";
const lang = ref(localStorage.getItem("lang")); const lang = ref(localStorage.getItem("lang"));
const translateLang = ref(localStorage.getItem("translateLang"));
const fontSize = ref(localStorage.getItem("fontSize")); const fontSize = ref(localStorage.getItem("fontSize"));
const useSystemFont = ref(localStorage.getItem("useSystemFont") != null); const useSystemFont = ref(localStorage.getItem("useSystemFont") != null);
@ -357,6 +370,9 @@ const showAdminUpdates = computed(
const showTimelineReplies = computed( const showTimelineReplies = computed(
defaultStore.makeGetterSetter("showTimelineReplies"), defaultStore.makeGetterSetter("showTimelineReplies"),
); );
const detectPostLanguage = computed(
defaultStore.makeGetterSetter("detectPostLanguage"),
);
watch(swipeOnDesktop, () => { watch(swipeOnDesktop, () => {
defaultStore.set("swipeOnMobile", true); defaultStore.set("swipeOnMobile", true);
@ -367,6 +383,10 @@ watch(lang, () => {
localStorage.removeItem("locale"); localStorage.removeItem("locale");
}); });
watch(translateLang, () => {
localStorage.setItem("translateLang", translateLang.value as string);
});
watch(fontSize, () => { watch(fontSize, () => {
if (fontSize.value == null) { if (fontSize.value == null) {
localStorage.removeItem("fontSize"); localStorage.removeItem("fontSize");
@ -386,6 +406,7 @@ watch(useSystemFont, () => {
watch( watch(
[ [
lang, lang,
translateLang,
fontSize, fontSize,
useSystemFont, useSystemFont,
enableInfiniteScroll, enableInfiniteScroll,

View file

@ -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> { async function translate(): Promise<void> {
const translateLang = localStorage.getItem("translateLang");
const lang = localStorage.getItem("lang");
if (props.translation.value != null) return; if (props.translation.value != null) return;
props.translating.value = true; props.translating.value = true;
const res = await os.api("notes/translate", { props.translation.value = await translate_(
noteId: appearNote.id, appearNote.id,
targetLang: localStorage.getItem("lang") || navigator.language, 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.translating.value = false;
props.translation.value = res;
} }
let menu; let menu;

View file

@ -1,6 +1,5 @@
import { markRaw, ref } from "vue"; import { markRaw, ref } from "vue";
import { Storage } from "./pizzax"; import { Storage } from "./pizzax";
import { Theme } from "./scripts/theme";
export const postFormActions = []; export const postFormActions = [];
export const userActions = []; export const userActions = [];
@ -346,6 +345,10 @@ export const defaultStore = markRaw(
where: "account", where: "account",
default: true, default: true,
}, },
detectPostLanguage: {
where: "deviceAccount",
default: true,
},
}), }),
); );

View file

@ -830,6 +830,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
@ -18042,6 +18045,12 @@ packages:
/tinycolor2@1.6.0: /tinycolor2@1.6.0:
resolution: {integrity: sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==} 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: /titleize@3.0.0:
resolution: {integrity: sha512-KxVu8EYHDPBdUYdKZdKtU2aj2XfEx9AfjXxE/Aj0vT06w2icA09Vus1rh6eSu1y01akYg6BjIK/hxyLJINoMLQ==} resolution: {integrity: sha512-KxVu8EYHDPBdUYdKZdKtU2aj2XfEx9AfjXxE/Aj0vT06w2icA09Vus1rh6eSu1y01akYg6BjIK/hxyLJINoMLQ==}
engines: {node: '>=12'} engines: {node: '>=12'}