parent
1cd6ba3c1d
commit
cced83024b
11 changed files with 210 additions and 17 deletions
|
@ -10,6 +10,8 @@
|
||||||
## 12.x.x (unreleased)
|
## 12.x.x (unreleased)
|
||||||
|
|
||||||
### Improvements
|
### Improvements
|
||||||
|
- ノートの翻訳機能を追加
|
||||||
|
- 有効にするには、サーバー管理者がDeepLの無料アカウントを登録し、取得した認証キーを「インスタンス設定 > その他 > DeepL Auth Key」に設定する必要があります。
|
||||||
- Misskey更新時にダイアログを表示するように
|
- Misskey更新時にダイアログを表示するように
|
||||||
- ジョブキューウィジェットに警報音を鳴らす設定を追加
|
- ジョブキューウィジェットに警報音を鳴らす設定を追加
|
||||||
‐ UIデザインの調整
|
‐ UIデザインの調整
|
||||||
|
|
|
@ -775,6 +775,8 @@ useBlurEffect: "UIにぼかし効果を使用"
|
||||||
learnMore: "詳しく"
|
learnMore: "詳しく"
|
||||||
misskeyUpdated: "Misskeyが更新されました!"
|
misskeyUpdated: "Misskeyが更新されました!"
|
||||||
whatIsNew: "更新情報を見る"
|
whatIsNew: "更新情報を見る"
|
||||||
|
translate: "翻訳"
|
||||||
|
translatedFrom: "{x}から翻訳"
|
||||||
|
|
||||||
_docs:
|
_docs:
|
||||||
continueReading: "続きを読む"
|
continueReading: "続きを読む"
|
||||||
|
|
14
migration/1629024377804-deepl-integration.ts
Normal file
14
migration/1629024377804-deepl-integration.ts
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
import {MigrationInterface, QueryRunner} from "typeorm";
|
||||||
|
|
||||||
|
export class deeplIntegration1629024377804 implements MigrationInterface {
|
||||||
|
name = 'deeplIntegration1629024377804'
|
||||||
|
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`ALTER TABLE "meta" ADD "deeplAuthKey" character varying(128)`);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "deeplAuthKey"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="yxspomdl" :class="{ inline, colored }">
|
<div class="yxspomdl" :class="{ inline, colored, mini }">
|
||||||
<div class="ring"></div>
|
<div class="ring"></div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -18,7 +18,12 @@ export default defineComponent({
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
default: true
|
default: true
|
||||||
}
|
},
|
||||||
|
mini: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
@ -38,6 +43,8 @@ export default defineComponent({
|
||||||
text-align: center;
|
text-align: center;
|
||||||
cursor: wait;
|
cursor: wait;
|
||||||
|
|
||||||
|
--size: 48px;
|
||||||
|
|
||||||
&.colored {
|
&.colored {
|
||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
}
|
}
|
||||||
|
@ -45,19 +52,12 @@ export default defineComponent({
|
||||||
&.inline {
|
&.inline {
|
||||||
display: inline;
|
display: inline;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
--size: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
> .ring:after {
|
&.mini {
|
||||||
width: 32px;
|
padding: 16px;
|
||||||
height: 32px;
|
--size: 32px;
|
||||||
}
|
|
||||||
|
|
||||||
> .ring {
|
|
||||||
&:before,
|
|
||||||
&:after {
|
|
||||||
width: 32px;
|
|
||||||
height: 32px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
> .ring {
|
> .ring {
|
||||||
|
@ -70,8 +70,8 @@ export default defineComponent({
|
||||||
content: " ";
|
content: " ";
|
||||||
display: block;
|
display: block;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
width: 48px;
|
width: var(--size);
|
||||||
height: 48px;
|
height: var(--size);
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
border: solid 4px;
|
border: solid 4px;
|
||||||
}
|
}
|
||||||
|
|
|
@ -67,6 +67,13 @@
|
||||||
<MkA class="reply" v-if="appearNote.replyId" :to="`/notes/${appearNote.replyId}`"><i class="fas fa-reply"></i></MkA>
|
<MkA class="reply" v-if="appearNote.replyId" :to="`/notes/${appearNote.replyId}`"><i class="fas fa-reply"></i></MkA>
|
||||||
<Mfm v-if="appearNote.text" :text="appearNote.text" :author="appearNote.user" :i="$i" :custom-emojis="appearNote.emojis"/>
|
<Mfm v-if="appearNote.text" :text="appearNote.text" :author="appearNote.user" :i="$i" :custom-emojis="appearNote.emojis"/>
|
||||||
<a class="rp" v-if="appearNote.renote != null">RN:</a>
|
<a class="rp" v-if="appearNote.renote != null">RN:</a>
|
||||||
|
<div class="translation" v-if="translating || translation">
|
||||||
|
<MkLoading v-if="translating" mini/>
|
||||||
|
<div class="translated" v-else>
|
||||||
|
<b>{{ $t('translatedFrom', { x: translation.sourceLang }) }}:</b>
|
||||||
|
{{ translation.text }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="files" v-if="appearNote.files.length > 0">
|
<div class="files" v-if="appearNote.files.length > 0">
|
||||||
<XMediaList :media-list="appearNote.files"/>
|
<XMediaList :media-list="appearNote.files"/>
|
||||||
|
@ -178,6 +185,8 @@ export default defineComponent({
|
||||||
showContent: false,
|
showContent: false,
|
||||||
isDeleted: false,
|
isDeleted: false,
|
||||||
muted: false,
|
muted: false,
|
||||||
|
translation: null,
|
||||||
|
translating: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -619,6 +628,11 @@ export default defineComponent({
|
||||||
text: this.$ts.share,
|
text: this.$ts.share,
|
||||||
action: this.share
|
action: this.share
|
||||||
},
|
},
|
||||||
|
this.$instance.translatorAvailable ? {
|
||||||
|
icon: 'fas fa-language',
|
||||||
|
text: this.$ts.translate,
|
||||||
|
action: this.translate
|
||||||
|
} : undefined,
|
||||||
null,
|
null,
|
||||||
statePromise.then(state => state.isFavorited ? {
|
statePromise.then(state => state.isFavorited ? {
|
||||||
icon: 'fas fa-star',
|
icon: 'fas fa-star',
|
||||||
|
@ -852,6 +866,17 @@ export default defineComponent({
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async translate() {
|
||||||
|
if (this.translation != null) return;
|
||||||
|
this.translating = true;
|
||||||
|
const res = await os.api('notes/translate', {
|
||||||
|
noteId: this.appearNote.id,
|
||||||
|
targetLang: localStorage.getItem('lang') || navigator.language,
|
||||||
|
});
|
||||||
|
this.translating = false;
|
||||||
|
this.translation = res;
|
||||||
|
},
|
||||||
|
|
||||||
focus() {
|
focus() {
|
||||||
this.$el.focus();
|
this.$el.focus();
|
||||||
},
|
},
|
||||||
|
@ -1050,6 +1075,13 @@ export default defineComponent({
|
||||||
font-style: oblique;
|
font-style: oblique;
|
||||||
color: var(--renote);
|
color: var(--renote);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
> .translation {
|
||||||
|
border: solid 0.5px var(--divider);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
padding: 12px;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
> .url-preview {
|
> .url-preview {
|
||||||
|
|
|
@ -51,6 +51,13 @@
|
||||||
<MkA class="reply" v-if="appearNote.replyId" :to="`/notes/${appearNote.replyId}`"><i class="fas fa-reply"></i></MkA>
|
<MkA class="reply" v-if="appearNote.replyId" :to="`/notes/${appearNote.replyId}`"><i class="fas fa-reply"></i></MkA>
|
||||||
<Mfm v-if="appearNote.text" :text="appearNote.text" :author="appearNote.user" :i="$i" :custom-emojis="appearNote.emojis"/>
|
<Mfm v-if="appearNote.text" :text="appearNote.text" :author="appearNote.user" :i="$i" :custom-emojis="appearNote.emojis"/>
|
||||||
<a class="rp" v-if="appearNote.renote != null">RN:</a>
|
<a class="rp" v-if="appearNote.renote != null">RN:</a>
|
||||||
|
<div class="translation" v-if="translating || translation">
|
||||||
|
<MkLoading v-if="translating" mini/>
|
||||||
|
<div class="translated" v-else>
|
||||||
|
<b>{{ $t('translatedFrom', { x: translation.sourceLang }) }}:</b>
|
||||||
|
{{ translation.text }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="files" v-if="appearNote.files.length > 0">
|
<div class="files" v-if="appearNote.files.length > 0">
|
||||||
<XMediaList :media-list="appearNote.files"/>
|
<XMediaList :media-list="appearNote.files"/>
|
||||||
|
@ -164,6 +171,8 @@ export default defineComponent({
|
||||||
collapsed: false,
|
collapsed: false,
|
||||||
isDeleted: false,
|
isDeleted: false,
|
||||||
muted: false,
|
muted: false,
|
||||||
|
translation: null,
|
||||||
|
translating: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -594,6 +603,11 @@ export default defineComponent({
|
||||||
text: this.$ts.share,
|
text: this.$ts.share,
|
||||||
action: this.share
|
action: this.share
|
||||||
},
|
},
|
||||||
|
this.$instance.translatorAvailable ? {
|
||||||
|
icon: 'fas fa-language',
|
||||||
|
text: this.$ts.translate,
|
||||||
|
action: this.translate
|
||||||
|
} : undefined,
|
||||||
null,
|
null,
|
||||||
statePromise.then(state => state.isFavorited ? {
|
statePromise.then(state => state.isFavorited ? {
|
||||||
icon: 'fas fa-star',
|
icon: 'fas fa-star',
|
||||||
|
@ -827,6 +841,17 @@ export default defineComponent({
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async translate() {
|
||||||
|
if (this.translation != null) return;
|
||||||
|
this.translating = true;
|
||||||
|
const res = await os.api('notes/translate', {
|
||||||
|
noteId: this.appearNote.id,
|
||||||
|
targetLang: localStorage.getItem('lang') || navigator.language,
|
||||||
|
});
|
||||||
|
this.translating = false;
|
||||||
|
this.translation = res;
|
||||||
|
},
|
||||||
|
|
||||||
focus() {
|
focus() {
|
||||||
this.$el.focus();
|
this.$el.focus();
|
||||||
},
|
},
|
||||||
|
@ -1053,6 +1078,13 @@ export default defineComponent({
|
||||||
font-style: oblique;
|
font-style: oblique;
|
||||||
color: var(--renote);
|
color: var(--renote);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
> .translation {
|
||||||
|
border: solid 0.5px var(--divider);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
padding: 12px;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
> .url-preview {
|
> .url-preview {
|
||||||
|
|
|
@ -7,7 +7,12 @@
|
||||||
Summaly Proxy URL
|
Summaly Proxy URL
|
||||||
</FormInput>
|
</FormInput>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
<FormGroup>
|
||||||
|
<FormInput v-model:value="deeplAuthKey">
|
||||||
|
<template #prefix><i class="fas fa-key"></i></template>
|
||||||
|
DeepL Auth Key
|
||||||
|
</FormInput>
|
||||||
|
</FormGroup>
|
||||||
<FormButton @click="save" primary><i class="fas fa-save"></i> {{ $ts.save }}</FormButton>
|
<FormButton @click="save" primary><i class="fas fa-save"></i> {{ $ts.save }}</FormButton>
|
||||||
</FormSuspense>
|
</FormSuspense>
|
||||||
</FormBase>
|
</FormBase>
|
||||||
|
@ -44,6 +49,7 @@ export default defineComponent({
|
||||||
icon: 'fas fa-cogs'
|
icon: 'fas fa-cogs'
|
||||||
},
|
},
|
||||||
summalyProxy: '',
|
summalyProxy: '',
|
||||||
|
deeplAuthKey: '',
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -55,10 +61,12 @@ export default defineComponent({
|
||||||
async init() {
|
async init() {
|
||||||
const meta = await os.api('meta', { detail: true });
|
const meta = await os.api('meta', { detail: true });
|
||||||
this.summalyProxy = meta.summalyProxy;
|
this.summalyProxy = meta.summalyProxy;
|
||||||
|
this.deeplAuthKey = meta.deeplAuthKey;
|
||||||
},
|
},
|
||||||
save() {
|
save() {
|
||||||
os.apiWithDialog('admin/update-meta', {
|
os.apiWithDialog('admin/update-meta', {
|
||||||
summalyProxy: this.summalyProxy,
|
summalyProxy: this.summalyProxy,
|
||||||
|
deeplAuthKey: this.deeplAuthKey,
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
fetchInstance();
|
fetchInstance();
|
||||||
});
|
});
|
||||||
|
|
|
@ -313,6 +313,12 @@ export class Meta {
|
||||||
})
|
})
|
||||||
public discordClientSecret: string | null;
|
public discordClientSecret: string | null;
|
||||||
|
|
||||||
|
@Column('varchar', {
|
||||||
|
length: 128,
|
||||||
|
nullable: true
|
||||||
|
})
|
||||||
|
public deeplAuthKey: string | null;
|
||||||
|
|
||||||
@Column('varchar', {
|
@Column('varchar', {
|
||||||
length: 512,
|
length: 512,
|
||||||
nullable: true
|
nullable: true
|
||||||
|
|
|
@ -145,6 +145,10 @@ export const meta = {
|
||||||
validator: $.optional.nullable.str,
|
validator: $.optional.nullable.str,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
deeplAuthKey: {
|
||||||
|
validator: $.optional.nullable.str,
|
||||||
|
},
|
||||||
|
|
||||||
enableTwitterIntegration: {
|
enableTwitterIntegration: {
|
||||||
validator: $.optional.bool,
|
validator: $.optional.bool,
|
||||||
},
|
},
|
||||||
|
@ -562,6 +566,14 @@ export default define(meta, async (ps, me) => {
|
||||||
set.objectStorageS3ForcePathStyle = ps.objectStorageS3ForcePathStyle;
|
set.objectStorageS3ForcePathStyle = ps.objectStorageS3ForcePathStyle;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (ps.deeplAuthKey !== undefined) {
|
||||||
|
if (ps.deeplAuthKey === '') {
|
||||||
|
set.deeplAuthKey = null;
|
||||||
|
} else {
|
||||||
|
set.deeplAuthKey = ps.deeplAuthKey;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
await getConnection().transaction(async transactionalEntityManager => {
|
await getConnection().transaction(async transactionalEntityManager => {
|
||||||
const meta = await transactionalEntityManager.findOne(Meta, {
|
const meta = await transactionalEntityManager.findOne(Meta, {
|
||||||
order: {
|
order: {
|
||||||
|
|
|
@ -232,6 +232,10 @@ export const meta = {
|
||||||
type: 'boolean' as const,
|
type: 'boolean' as const,
|
||||||
optional: false as const, nullable: false as const
|
optional: false as const, nullable: false as const
|
||||||
},
|
},
|
||||||
|
translatorAvailable: {
|
||||||
|
type: 'boolean' as const,
|
||||||
|
optional: false as const, nullable: false as const
|
||||||
|
},
|
||||||
proxyAccountName: {
|
proxyAccountName: {
|
||||||
type: 'string' as const,
|
type: 'string' as const,
|
||||||
optional: false as const, nullable: true as const
|
optional: false as const, nullable: true as const
|
||||||
|
@ -512,6 +516,8 @@ export default define(meta, async (ps, me) => {
|
||||||
|
|
||||||
enableServiceWorker: instance.enableServiceWorker,
|
enableServiceWorker: instance.enableServiceWorker,
|
||||||
|
|
||||||
|
translatorAvailable: instance.deeplAuthKey != null,
|
||||||
|
|
||||||
...(ps.detail ? {
|
...(ps.detail ? {
|
||||||
pinnedPages: instance.pinnedPages,
|
pinnedPages: instance.pinnedPages,
|
||||||
pinnedClipId: instance.pinnedClipId,
|
pinnedClipId: instance.pinnedClipId,
|
||||||
|
|
79
src/server/api/endpoints/notes/translate.ts
Normal file
79
src/server/api/endpoints/notes/translate.ts
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
import $ from 'cafy';
|
||||||
|
import { ID } from '@/misc/cafy-id';
|
||||||
|
import define from '../../define';
|
||||||
|
import { getNote } from '../../common/getters';
|
||||||
|
import { ApiError } from '../../error';
|
||||||
|
import fetch from 'node-fetch';
|
||||||
|
import config from '@/config';
|
||||||
|
import { getAgentByUrl } from '@/misc/fetch';
|
||||||
|
import { URLSearchParams } from 'url';
|
||||||
|
import { fetchMeta } from '@/misc/fetch-meta';
|
||||||
|
|
||||||
|
export const meta = {
|
||||||
|
tags: ['notes'],
|
||||||
|
|
||||||
|
requireCredential: false as const,
|
||||||
|
|
||||||
|
params: {
|
||||||
|
noteId: {
|
||||||
|
validator: $.type(ID),
|
||||||
|
},
|
||||||
|
targetLang: {
|
||||||
|
validator: $.str,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
res: {
|
||||||
|
type: 'object' as const,
|
||||||
|
optional: false as const, nullable: false as const,
|
||||||
|
},
|
||||||
|
|
||||||
|
errors: {
|
||||||
|
noSuchNote: {
|
||||||
|
message: 'No such note.',
|
||||||
|
code: 'NO_SUCH_NOTE',
|
||||||
|
id: 'bea9b03f-36e0-49c5-a4db-627a029f8971'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default define(meta, async (ps, user) => {
|
||||||
|
const note = await getNote(ps.noteId).catch(e => {
|
||||||
|
if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
|
||||||
|
throw e;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (note.text == null) {
|
||||||
|
return 204;
|
||||||
|
}
|
||||||
|
|
||||||
|
const instance = await fetchMeta();
|
||||||
|
|
||||||
|
if (instance.deeplAuthKey == null) {
|
||||||
|
return 204; // TODO: 良い感じのエラー返す
|
||||||
|
}
|
||||||
|
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
params.append('auth_key', instance.deeplAuthKey);
|
||||||
|
params.append('text', note.text);
|
||||||
|
params.append('target_lang', ps.targetLang);
|
||||||
|
|
||||||
|
const res = await fetch('https://api-free.deepl.com/v2/translate', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
'User-Agent': config.userAgent,
|
||||||
|
Accept: 'application/json, */*'
|
||||||
|
},
|
||||||
|
body: params,
|
||||||
|
timeout: 10000,
|
||||||
|
agent: getAgentByUrl,
|
||||||
|
});
|
||||||
|
|
||||||
|
const json = await res.json();
|
||||||
|
|
||||||
|
return {
|
||||||
|
sourceLang: json.translations[0].detected_source_language,
|
||||||
|
text: json.translations[0].text
|
||||||
|
};
|
||||||
|
});
|
Loading…
Reference in a new issue