parent
1cd6ba3c1d
commit
cced83024b
|
@ -10,6 +10,8 @@
|
|||
## 12.x.x (unreleased)
|
||||
|
||||
### Improvements
|
||||
- ノートの翻訳機能を追加
|
||||
- 有効にするには、サーバー管理者がDeepLの無料アカウントを登録し、取得した認証キーを「インスタンス設定 > その他 > DeepL Auth Key」に設定する必要があります。
|
||||
- Misskey更新時にダイアログを表示するように
|
||||
- ジョブキューウィジェットに警報音を鳴らす設定を追加
|
||||
‐ UIデザインの調整
|
||||
|
|
|
@ -775,6 +775,8 @@ useBlurEffect: "UIにぼかし効果を使用"
|
|||
learnMore: "詳しく"
|
||||
misskeyUpdated: "Misskeyが更新されました!"
|
||||
whatIsNew: "更新情報を見る"
|
||||
translate: "翻訳"
|
||||
translatedFrom: "{x}から翻訳"
|
||||
|
||||
_docs:
|
||||
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>
|
||||
<div class="yxspomdl" :class="{ inline, colored }">
|
||||
<div class="yxspomdl" :class="{ inline, colored, mini }">
|
||||
<div class="ring"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -18,7 +18,12 @@ export default defineComponent({
|
|||
type: Boolean,
|
||||
required: false,
|
||||
default: true
|
||||
}
|
||||
},
|
||||
mini: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
@ -38,6 +43,8 @@ export default defineComponent({
|
|||
text-align: center;
|
||||
cursor: wait;
|
||||
|
||||
--size: 48px;
|
||||
|
||||
&.colored {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
@ -45,19 +52,12 @@ export default defineComponent({
|
|||
&.inline {
|
||||
display: inline;
|
||||
padding: 0;
|
||||
|
||||
> .ring:after {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
--size: 32px;
|
||||
}
|
||||
|
||||
> .ring {
|
||||
&:before,
|
||||
&:after {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
}
|
||||
&.mini {
|
||||
padding: 16px;
|
||||
--size: 32px;
|
||||
}
|
||||
|
||||
> .ring {
|
||||
|
@ -70,8 +70,8 @@ export default defineComponent({
|
|||
content: " ";
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
width: var(--size);
|
||||
height: var(--size);
|
||||
border-radius: 50%;
|
||||
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>
|
||||
<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>
|
||||
<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 class="files" v-if="appearNote.files.length > 0">
|
||||
<XMediaList :media-list="appearNote.files"/>
|
||||
|
@ -178,6 +185,8 @@ export default defineComponent({
|
|||
showContent: false,
|
||||
isDeleted: false,
|
||||
muted: false,
|
||||
translation: null,
|
||||
translating: false,
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -619,6 +628,11 @@ export default defineComponent({
|
|||
text: this.$ts.share,
|
||||
action: this.share
|
||||
},
|
||||
this.$instance.translatorAvailable ? {
|
||||
icon: 'fas fa-language',
|
||||
text: this.$ts.translate,
|
||||
action: this.translate
|
||||
} : undefined,
|
||||
null,
|
||||
statePromise.then(state => state.isFavorited ? {
|
||||
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() {
|
||||
this.$el.focus();
|
||||
},
|
||||
|
@ -1050,6 +1075,13 @@ export default defineComponent({
|
|||
font-style: oblique;
|
||||
color: var(--renote);
|
||||
}
|
||||
|
||||
> .translation {
|
||||
border: solid 0.5px var(--divider);
|
||||
border-radius: var(--radius);
|
||||
padding: 12px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
> .url-preview {
|
||||
|
|
|
@ -51,6 +51,13 @@
|
|||
<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"/>
|
||||
<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 class="files" v-if="appearNote.files.length > 0">
|
||||
<XMediaList :media-list="appearNote.files"/>
|
||||
|
@ -164,6 +171,8 @@ export default defineComponent({
|
|||
collapsed: false,
|
||||
isDeleted: false,
|
||||
muted: false,
|
||||
translation: null,
|
||||
translating: false,
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -594,6 +603,11 @@ export default defineComponent({
|
|||
text: this.$ts.share,
|
||||
action: this.share
|
||||
},
|
||||
this.$instance.translatorAvailable ? {
|
||||
icon: 'fas fa-language',
|
||||
text: this.$ts.translate,
|
||||
action: this.translate
|
||||
} : undefined,
|
||||
null,
|
||||
statePromise.then(state => state.isFavorited ? {
|
||||
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() {
|
||||
this.$el.focus();
|
||||
},
|
||||
|
@ -1053,6 +1078,13 @@ export default defineComponent({
|
|||
font-style: oblique;
|
||||
color: var(--renote);
|
||||
}
|
||||
|
||||
> .translation {
|
||||
border: solid 0.5px var(--divider);
|
||||
border-radius: var(--radius);
|
||||
padding: 12px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
> .url-preview {
|
||||
|
|
|
@ -7,7 +7,12 @@
|
|||
Summaly Proxy URL
|
||||
</FormInput>
|
||||
</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>
|
||||
</FormSuspense>
|
||||
</FormBase>
|
||||
|
@ -44,6 +49,7 @@ export default defineComponent({
|
|||
icon: 'fas fa-cogs'
|
||||
},
|
||||
summalyProxy: '',
|
||||
deeplAuthKey: '',
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -55,10 +61,12 @@ export default defineComponent({
|
|||
async init() {
|
||||
const meta = await os.api('meta', { detail: true });
|
||||
this.summalyProxy = meta.summalyProxy;
|
||||
this.deeplAuthKey = meta.deeplAuthKey;
|
||||
},
|
||||
save() {
|
||||
os.apiWithDialog('admin/update-meta', {
|
||||
summalyProxy: this.summalyProxy,
|
||||
deeplAuthKey: this.deeplAuthKey,
|
||||
}).then(() => {
|
||||
fetchInstance();
|
||||
});
|
||||
|
|
|
@ -313,6 +313,12 @@ export class Meta {
|
|||
})
|
||||
public discordClientSecret: string | null;
|
||||
|
||||
@Column('varchar', {
|
||||
length: 128,
|
||||
nullable: true
|
||||
})
|
||||
public deeplAuthKey: string | null;
|
||||
|
||||
@Column('varchar', {
|
||||
length: 512,
|
||||
nullable: true
|
||||
|
|
|
@ -145,6 +145,10 @@ export const meta = {
|
|||
validator: $.optional.nullable.str,
|
||||
},
|
||||
|
||||
deeplAuthKey: {
|
||||
validator: $.optional.nullable.str,
|
||||
},
|
||||
|
||||
enableTwitterIntegration: {
|
||||
validator: $.optional.bool,
|
||||
},
|
||||
|
@ -562,6 +566,14 @@ export default define(meta, async (ps, me) => {
|
|||
set.objectStorageS3ForcePathStyle = ps.objectStorageS3ForcePathStyle;
|
||||
}
|
||||
|
||||
if (ps.deeplAuthKey !== undefined) {
|
||||
if (ps.deeplAuthKey === '') {
|
||||
set.deeplAuthKey = null;
|
||||
} else {
|
||||
set.deeplAuthKey = ps.deeplAuthKey;
|
||||
}
|
||||
}
|
||||
|
||||
await getConnection().transaction(async transactionalEntityManager => {
|
||||
const meta = await transactionalEntityManager.findOne(Meta, {
|
||||
order: {
|
||||
|
|
|
@ -232,6 +232,10 @@ export const meta = {
|
|||
type: 'boolean' as const,
|
||||
optional: false as const, nullable: false as const
|
||||
},
|
||||
translatorAvailable: {
|
||||
type: 'boolean' as const,
|
||||
optional: false as const, nullable: false as const
|
||||
},
|
||||
proxyAccountName: {
|
||||
type: 'string' as const,
|
||||
optional: false as const, nullable: true as const
|
||||
|
@ -512,6 +516,8 @@ export default define(meta, async (ps, me) => {
|
|||
|
||||
enableServiceWorker: instance.enableServiceWorker,
|
||||
|
||||
translatorAvailable: instance.deeplAuthKey != null,
|
||||
|
||||
...(ps.detail ? {
|
||||
pinnedPages: instance.pinnedPages,
|
||||
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