From 40599190f731fb2ce81fe547438eab0e17837852 Mon Sep 17 00:00:00 2001 From: Marie Date: Thu, 6 Mar 2025 01:03:02 +0100 Subject: [PATCH 1/3] add: libretranslate --- .../migration/1741215877000-libetranslate.js | 18 +++ .../src/core/entities/MetaEntityService.ts | 2 +- packages/backend/src/models/Meta.ts | 12 ++ .../src/server/api/endpoints/admin/meta.ts | 12 +- .../server/api/endpoints/admin/update-meta.ts | 18 +++ .../server/api/endpoints/notes/translate.ts | 104 ++++++++++++------ .../src/pages/admin/external-services.vue | 79 +++++++++---- packages/misskey-js/src/autogen/types.ts | 4 + 8 files changed, 190 insertions(+), 59 deletions(-) create mode 100644 packages/backend/migration/1741215877000-libetranslate.js diff --git a/packages/backend/migration/1741215877000-libetranslate.js b/packages/backend/migration/1741215877000-libetranslate.js new file mode 100644 index 0000000000..a2345ea5a4 --- /dev/null +++ b/packages/backend/migration/1741215877000-libetranslate.js @@ -0,0 +1,18 @@ +/* + * SPDX-FileCopyrightText: marie and other Sharkey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export class Libetranslate1741215877000 { + name = 'Libretranslate1741215877000'; + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" ADD "libreTranslateURL" character varying(1024)`); + await queryRunner.query(`ALTER TABLE "meta" ADD "libreTranslateKey" character varying(1024)`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "libreTranslateURL"`); + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "libreTranslateKey"`); + } +} diff --git a/packages/backend/src/core/entities/MetaEntityService.ts b/packages/backend/src/core/entities/MetaEntityService.ts index 84d591ce7a..a7679d06aa 100644 --- a/packages/backend/src/core/entities/MetaEntityService.ts +++ b/packages/backend/src/core/entities/MetaEntityService.ts @@ -135,7 +135,7 @@ export class MetaEntityService { enableEmail: instance.enableEmail, enableServiceWorker: instance.enableServiceWorker, - translatorAvailable: instance.deeplAuthKey != null || instance.deeplFreeMode && instance.deeplFreeInstance != null, + translatorAvailable: instance.deeplAuthKey != null || instance.libreTranslateURL != null || instance.deeplFreeMode && instance.deeplFreeInstance != null, serverRules: instance.serverRules, diff --git a/packages/backend/src/models/Meta.ts b/packages/backend/src/models/Meta.ts index a224117676..0f1f4069ff 100644 --- a/packages/backend/src/models/Meta.ts +++ b/packages/backend/src/models/Meta.ts @@ -403,6 +403,18 @@ export class MiMeta { }) public deeplFreeInstance: string | null; + @Column('varchar', { + length: 1024, + nullable: true, + }) + public libreTranslateURL: string | null; + + @Column('varchar', { + length: 1024, + nullable: true, + }) + public libreTranslateKey: string | null; + @Column('varchar', { length: 1024, nullable: true, diff --git a/packages/backend/src/server/api/endpoints/admin/meta.ts b/packages/backend/src/server/api/endpoints/admin/meta.ts index 436dcf27cb..d581c07e8c 100644 --- a/packages/backend/src/server/api/endpoints/admin/meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/meta.ts @@ -459,6 +459,14 @@ export const meta = { type: 'string', optional: false, nullable: true, }, + libreTranslateURL: { + type: 'string', + optional: false, nullable: true, + }, + libreTranslateKey: { + type: 'string', + optional: false, nullable: true, + }, defaultDarkTheme: { type: 'string', optional: false, nullable: true, @@ -652,7 +660,7 @@ export default class extends Endpoint { // eslint- defaultLike: instance.defaultLike, enableEmail: instance.enableEmail, enableServiceWorker: instance.enableServiceWorker, - translatorAvailable: instance.deeplAuthKey != null, + translatorAvailable: instance.deeplAuthKey != null || instance.libreTranslateURL != null || instance.deeplFreeMode && instance.deeplFreeInstance != null, cacheRemoteFiles: instance.cacheRemoteFiles, cacheRemoteSensitiveFiles: instance.cacheRemoteSensitiveFiles, pinnedUsers: instance.pinnedUsers, @@ -700,6 +708,8 @@ export default class extends Endpoint { // eslint- deeplIsPro: instance.deeplIsPro, deeplFreeMode: instance.deeplFreeMode, deeplFreeInstance: instance.deeplFreeInstance, + libreTranslateURL: instance.libreTranslateURL, + libreTranslateKey: instance.libreTranslateKey, enableIpLogging: instance.enableIpLogging, enableActiveEmailValidation: instance.enableActiveEmailValidation, enableVerifymailApi: instance.enableVerifymailApi, diff --git a/packages/backend/src/server/api/endpoints/admin/update-meta.ts b/packages/backend/src/server/api/endpoints/admin/update-meta.ts index b3733d3d39..f6ce86790a 100644 --- a/packages/backend/src/server/api/endpoints/admin/update-meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/update-meta.ts @@ -107,6 +107,8 @@ export const paramDef = { deeplIsPro: { type: 'boolean' }, deeplFreeMode: { type: 'boolean' }, deeplFreeInstance: { type: 'string', nullable: true }, + libreTranslateURL: { type: 'string', nullable: true }, + libreTranslateKey: { type: 'string', nullable: true }, enableEmail: { type: 'boolean' }, email: { type: 'string', nullable: true }, smtpSecure: { type: 'boolean' }, @@ -577,6 +579,22 @@ export default class extends Endpoint { // eslint- } } + if (ps.libreTranslateURL !== undefined) { + if (ps.libreTranslateURL === '') { + set.libreTranslateURL = null; + } else { + set.libreTranslateURL = ps.libreTranslateURL; + } + } + + if (ps.libreTranslateKey !== undefined) { + if (ps.libreTranslateKey === '') { + set.libreTranslateKey = null; + } else { + set.libreTranslateKey = ps.libreTranslateKey; + } + } + if (ps.enableIpLogging !== undefined) { set.enableIpLogging = ps.enableIpLogging; } diff --git a/packages/backend/src/server/api/endpoints/notes/translate.ts b/packages/backend/src/server/api/endpoints/notes/translate.ts index 61a511510c..b59977b7dd 100644 --- a/packages/backend/src/server/api/endpoints/notes/translate.ts +++ b/packages/backend/src/server/api/endpoints/notes/translate.ts @@ -93,11 +93,11 @@ export default class extends Endpoint { // eslint- return; } - if (this.serverSettings.deeplAuthKey == null && !this.serverSettings.deeplFreeMode) { + if (this.serverSettings.deeplAuthKey == null && !this.serverSettings.deeplFreeMode && !this.serverSettings.libreTranslateURL) { throw new ApiError(meta.errors.unavailable); } - if (this.serverSettings.deeplFreeMode && !this.serverSettings.deeplFreeInstance) { + if (this.serverSettings.deeplFreeMode && !this.serverSettings.deeplFreeInstance && !this.serverSettings.libreTranslateURL) { throw new ApiError(meta.errors.unavailable); } @@ -105,40 +105,76 @@ export default class extends Endpoint { // eslint- if (targetLang.includes('-')) targetLang = targetLang.split('-')[0]; const params = new URLSearchParams(); - if (this.serverSettings.deeplAuthKey) params.append('auth_key', this.serverSettings.deeplAuthKey); - params.append('text', note.text); - params.append('target_lang', targetLang); - const endpoint = this.serverSettings.deeplFreeMode && this.serverSettings.deeplFreeInstance ? this.serverSettings.deeplFreeInstance : this.serverSettings.deeplIsPro ? 'https://api.deepl.com/v2/translate' : 'https://api-free.deepl.com/v2/translate'; + // DeepL/DeepLX handling + if (this.serverSettings.deeplAuthKey != null || this.serverSettings.deeplFreeMode) { + if (this.serverSettings.deeplAuthKey) params.append('auth_key', this.serverSettings.deeplAuthKey); + params.append('text', note.text); + params.append('target_lang', targetLang); + const endpoint = this.serverSettings.deeplFreeMode && this.serverSettings.deeplFreeInstance ? this.serverSettings.deeplFreeInstance : this.serverSettings.deeplIsPro ? 'https://api.deepl.com/v2/translate' : 'https://api-free.deepl.com/v2/translate'; + + const res = await this.httpRequestService.send(endpoint, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + Accept: 'application/json, */*', + }, + body: params.toString(), + }); + if (this.serverSettings.deeplAuthKey) { + const json = (await res.json()) as { + translations: { + detected_source_language: string; + text: string; + }[]; + }; + + return { + sourceLang: json.translations[0].detected_source_language, + text: json.translations[0].text, + }; + } else { + const json = (await res.json()) as { + code: number, + message: string, + data: string, + source_lang: string, + target_lang: string, + alternatives: string[], + }; + + const languageNames = new Intl.DisplayNames(['en'], { + type: 'language', + }); + + return { + sourceLang: languageNames.of(json.source_lang), + text: json.data, + }; + } + } + + // LibreTranslate handling + if (this.serverSettings.libreTranslateURL) { + const res = await this.httpRequestService.send(this.serverSettings.libreTranslateURL, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json, */*', + }, + body: JSON.stringify({ + q: note.text, + source: 'auto', + target: targetLang, + format: 'text', + api_key: this.serverSettings.libreTranslateKey ?? '', + }), + }); - const res = await this.httpRequestService.send(endpoint, { - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - Accept: 'application/json, */*', - }, - body: params.toString(), - }); - if (this.serverSettings.deeplAuthKey) { const json = (await res.json()) as { - translations: { - detected_source_language: string; - text: string; - }[]; - }; - - return { - sourceLang: json.translations[0].detected_source_language, - text: json.translations[0].text, - }; - } else { - const json = (await res.json()) as { - code: number, - message: string, - data: string, - source_lang: string, - target_lang: string, alternatives: string[], + detectedLanguage: { [key: string]: string | number }, + translatedText: string, }; const languageNames = new Intl.DisplayNames(['en'], { @@ -146,8 +182,8 @@ export default class extends Endpoint { // eslint- }); return { - sourceLang: languageNames.of(json.source_lang), - text: json.data, + sourceLang: languageNames.of(json.detectedLanguage.language as string), + text: json.translatedText, }; } }); diff --git a/packages/frontend/src/pages/admin/external-services.vue b/packages/frontend/src/pages/admin/external-services.vue index 50e2c2dd51..8cff014104 100644 --- a/packages/frontend/src/pages/admin/external-services.vue +++ b/packages/frontend/src/pages/admin/external-services.vue @@ -8,30 +8,50 @@ SPDX-License-Identifier: AGPL-3.0-only - - +
+ + -
- - - - - - - +
+ + + + + + + - - - - - - - - + + + + + + + + - Save -
- + Save +
+
+ + + + +
+ + + + + + + + + + + Save +
+
+
@@ -51,10 +71,12 @@ import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import MkFolder from '@/components/MkFolder.vue'; -const deeplAuthKey = ref(''); +const deeplAuthKey = ref(''); const deeplIsPro = ref(false); const deeplFreeMode = ref(false); -const deeplFreeInstance = ref(''); +const deeplFreeInstance = ref(''); +const libreTranslateURL = ref(''); +const libreTranslateKey = ref(''); async function init() { const meta = await misskeyApi('admin/meta'); @@ -62,6 +84,8 @@ async function init() { deeplIsPro.value = meta.deeplIsPro; deeplFreeMode.value = meta.deeplFreeMode; deeplFreeInstance.value = meta.deeplFreeInstance; + libreTranslateURL.value = meta.libreTranslateURL; + libreTranslateKey.value = meta.libreTranslateKey; } function save_deepl() { @@ -75,6 +99,15 @@ function save_deepl() { }); } +function save_libre() { + os.apiWithDialog('admin/update-meta', { + libreTranslateURL: libreTranslateURL.value, + libreTranslateKey: libreTranslateKey.value, + }).then(() => { + fetchInstance(true); + }); +} + const headerActions = computed(() => []); const headerTabs = computed(() => []); diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts index d58607bb3b..c1156a7ffa 100644 --- a/packages/misskey-js/src/autogen/types.ts +++ b/packages/misskey-js/src/autogen/types.ts @@ -8826,6 +8826,8 @@ export type operations = { deeplIsPro: boolean; deeplFreeMode: boolean; deeplFreeInstance: string | null; + libreTranslateURL: string | null; + libreTranslateKey: string | null; defaultDarkTheme: string | null; defaultLightTheme: string | null; description: string | null; @@ -11401,6 +11403,8 @@ export type operations = { deeplIsPro?: boolean; deeplFreeMode?: boolean; deeplFreeInstance?: string | null; + libreTranslateURL?: string | null; + libreTranslateKey?: string | null; enableEmail?: boolean; email?: string | null; smtpSecure?: boolean; From 70d88805d516045124d67ea89fe14705a8cf8484 Mon Sep 17 00:00:00 2001 From: Marie Date: Thu, 6 Mar 2025 01:23:15 +0100 Subject: [PATCH 2/3] chore: typecheck error --- packages/backend/src/server/api/endpoints/notes/translate.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/backend/src/server/api/endpoints/notes/translate.ts b/packages/backend/src/server/api/endpoints/notes/translate.ts index b59977b7dd..c555fe296b 100644 --- a/packages/backend/src/server/api/endpoints/notes/translate.ts +++ b/packages/backend/src/server/api/endpoints/notes/translate.ts @@ -186,6 +186,8 @@ export default class extends Endpoint { // eslint- text: json.translatedText, }; } + + return; }); } } From 1f592f9166a44be6043d1a9b7319c205c0bb7984 Mon Sep 17 00:00:00 2001 From: Marie Date: Thu, 6 Mar 2025 16:48:32 +0100 Subject: [PATCH 3/3] upd: simplify checks --- .../server/api/endpoints/notes/translate.ts | 22 ++++++++----------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/packages/backend/src/server/api/endpoints/notes/translate.ts b/packages/backend/src/server/api/endpoints/notes/translate.ts index c555fe296b..39119bc206 100644 --- a/packages/backend/src/server/api/endpoints/notes/translate.ts +++ b/packages/backend/src/server/api/endpoints/notes/translate.ts @@ -93,25 +93,21 @@ export default class extends Endpoint { // eslint- return; } - if (this.serverSettings.deeplAuthKey == null && !this.serverSettings.deeplFreeMode && !this.serverSettings.libreTranslateURL) { - throw new ApiError(meta.errors.unavailable); - } - - if (this.serverSettings.deeplFreeMode && !this.serverSettings.deeplFreeInstance && !this.serverSettings.libreTranslateURL) { - throw new ApiError(meta.errors.unavailable); - } + const canDeeplFree = this.serverSettings.deeplFreeMode && !!this.serverSettings.deeplFreeInstance; + const canDeepl = !!this.serverSettings.deeplAuthKey || canDeeplFree; + const canLibre = !!this.serverSettings.libreTranslateURL; + if (!canDeepl && !canLibre) throw new ApiError(meta.errors.unavailable); let targetLang = ps.targetLang; if (targetLang.includes('-')) targetLang = targetLang.split('-')[0]; - const params = new URLSearchParams(); - // DeepL/DeepLX handling - if (this.serverSettings.deeplAuthKey != null || this.serverSettings.deeplFreeMode) { + if (canDeepl) { + const params = new URLSearchParams(); if (this.serverSettings.deeplAuthKey) params.append('auth_key', this.serverSettings.deeplAuthKey); params.append('text', note.text); params.append('target_lang', targetLang); - const endpoint = this.serverSettings.deeplFreeMode && this.serverSettings.deeplFreeInstance ? this.serverSettings.deeplFreeInstance : this.serverSettings.deeplIsPro ? 'https://api.deepl.com/v2/translate' : 'https://api-free.deepl.com/v2/translate'; + const endpoint = canDeeplFree ? this.serverSettings.deeplFreeInstance as string : this.serverSettings.deeplIsPro ? 'https://api.deepl.com/v2/translate' : 'https://api-free.deepl.com/v2/translate'; const res = await this.httpRequestService.send(endpoint, { method: 'POST', @@ -155,8 +151,8 @@ export default class extends Endpoint { // eslint- } // LibreTranslate handling - if (this.serverSettings.libreTranslateURL) { - const res = await this.httpRequestService.send(this.serverSettings.libreTranslateURL, { + if (canLibre) { + const res = await this.httpRequestService.send(this.serverSettings.libreTranslateURL as string, { method: 'POST', headers: { 'Content-Type': 'application/json',