summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/client/components/global/loading.vue32
-rw-r--r--src/client/components/note-detailed.vue32
-rw-r--r--src/client/components/note.vue32
-rw-r--r--src/client/pages/instance/other-settings.vue10
-rw-r--r--src/models/entities/meta.ts6
-rw-r--r--src/server/api/endpoints/admin/update-meta.ts12
-rw-r--r--src/server/api/endpoints/meta.ts6
-rw-r--r--src/server/api/endpoints/notes/translate.ts79
8 files changed, 192 insertions, 17 deletions
diff --git a/src/client/components/global/loading.vue b/src/client/components/global/loading.vue
index 9b810f0a16..7bde53c12e 100644
--- a/src/client/components/global/loading.vue
+++ b/src/client/components/global/loading.vue
@@ -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;
+ --size: 32px;
+ }
- > .ring:after {
- width: 32px;
- height: 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;
}
diff --git a/src/client/components/note-detailed.vue b/src/client/components/note-detailed.vue
index d601052927..a2460950cd 100644
--- a/src/client/components/note-detailed.vue
+++ b/src/client/components/note-detailed.vue
@@ -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 {
diff --git a/src/client/components/note.vue b/src/client/components/note.vue
index 873b96030a..38b529dd91 100644
--- a/src/client/components/note.vue
+++ b/src/client/components/note.vue
@@ -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 {
diff --git a/src/client/pages/instance/other-settings.vue b/src/client/pages/instance/other-settings.vue
index b3954149a8..8002528931 100644
--- a/src/client/pages/instance/other-settings.vue
+++ b/src/client/pages/instance/other-settings.vue
@@ -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();
});
diff --git a/src/models/entities/meta.ts b/src/models/entities/meta.ts
index d0b6ee7f2b..2a0632c87c 100644
--- a/src/models/entities/meta.ts
+++ b/src/models/entities/meta.ts
@@ -314,6 +314,12 @@ export class Meta {
public discordClientSecret: string | null;
@Column('varchar', {
+ length: 128,
+ nullable: true
+ })
+ public deeplAuthKey: string | null;
+
+ @Column('varchar', {
length: 512,
nullable: true
})
diff --git a/src/server/api/endpoints/admin/update-meta.ts b/src/server/api/endpoints/admin/update-meta.ts
index a18956b3f7..573f22822c 100644
--- a/src/server/api/endpoints/admin/update-meta.ts
+++ b/src/server/api/endpoints/admin/update-meta.ts
@@ -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: {
diff --git a/src/server/api/endpoints/meta.ts b/src/server/api/endpoints/meta.ts
index dd75149ad2..561d473d6f 100644
--- a/src/server/api/endpoints/meta.ts
+++ b/src/server/api/endpoints/meta.ts
@@ -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,
diff --git a/src/server/api/endpoints/notes/translate.ts b/src/server/api/endpoints/notes/translate.ts
new file mode 100644
index 0000000000..bbc11274ab
--- /dev/null
+++ b/src/server/api/endpoints/notes/translate.ts
@@ -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
+ };
+});