Merge branch 'feat/translations' into 'develop'
language for post translation & translate button See merge request firefish/firefish!10588 Co-authored-by: naskya <m@naskya.net>
This commit is contained in:
commit
820d5f77dc
9 changed files with 185 additions and 6 deletions
|
@ -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
|
||||||
|
|
|
@ -988,6 +988,8 @@ youHaveUnreadAnnouncements: "未読のお知らせがあります"
|
||||||
neverShow: "今後表示しない"
|
neverShow: "今後表示しない"
|
||||||
remindMeLater: "また後で"
|
remindMeLater: "また後で"
|
||||||
addRe: "閲覧注意の投稿への返信で、注釈の先頭に\"re:\"を追加する"
|
addRe: "閲覧注意の投稿への返信で、注釈の先頭に\"re:\"を追加する"
|
||||||
|
languageForTranslation: "投稿翻訳に使用する言語"
|
||||||
|
detectPostLanguage: "投稿の言語を自動検出し、外国語の投稿に翻訳ボタンを表示する"
|
||||||
|
|
||||||
_sensitiveMediaDetection:
|
_sensitiveMediaDetection:
|
||||||
description: "機械学習を使って自動でセンシティブなメディアを検出し、モデレーションに役立てられます。サーバーの負荷が少し増えます。"
|
description: "機械学習を使って自動でセンシティブなメディアを検出し、モデレーションに役立てられます。サーバーの負荷が少し増えます。"
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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),
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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,
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -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'}
|
||||||
|
|
Loading…
Reference in a new issue