diff options
| author | Hazelnoot <acomputerdog@gmail.com> | 2025-03-06 17:21:24 +0000 |
|---|---|---|
| committer | Hazelnoot <acomputerdog@gmail.com> | 2025-03-06 17:21:24 +0000 |
| commit | 49597e7e08f0509bdac35a70fbdedea26a707299 (patch) | |
| tree | e5f535d5ac6e4ddf2d06de7eb1eec5407c374762 | |
| parent | merge: Fix clickable notifications blocking clicks where they shouldn't (!937) (diff) | |
| parent | upd: simplify checks (diff) | |
| download | sharkey-49597e7e08f0509bdac35a70fbdedea26a707299.tar.gz sharkey-49597e7e08f0509bdac35a70fbdedea26a707299.tar.bz2 sharkey-49597e7e08f0509bdac35a70fbdedea26a707299.zip | |
merge: Add LibreTranslate as an option to External Services (!935)
View MR for information: https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/935
Closes #807
Approved-by: dakkar <dakkar@thenautilus.net>
Approved-by: Hazelnoot <acomputerdog@gmail.com>
8 files changed, 193 insertions, 64 deletions
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 @@ -407,6 +407,18 @@ export class MiMeta { length: 1024, nullable: true, }) + public libreTranslateURL: string | null; + + @Column('varchar', { + length: 1024, + nullable: true, + }) + public libreTranslateKey: string | null; + + @Column('varchar', { + length: 1024, + nullable: true, + }) public termsOfServiceUrl: string | null; @Column('varchar', { 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<typeof meta, typeof paramDef> { // 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<typeof meta, typeof paramDef> { // 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<typeof meta, typeof paramDef> { // 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..39119bc206 100644 --- a/packages/backend/src/server/api/endpoints/notes/translate.ts +++ b/packages/backend/src/server/api/endpoints/notes/translate.ts @@ -93,52 +93,84 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- return; } - if (this.serverSettings.deeplAuthKey == null && !this.serverSettings.deeplFreeMode) { - throw new ApiError(meta.errors.unavailable); - } - - if (this.serverSettings.deeplFreeMode && !this.serverSettings.deeplFreeInstance) { - 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(); - if (this.serverSettings.deeplAuthKey) params.append('auth_key', this.serverSettings.deeplAuthKey); - params.append('text', note.text); - params.append('target_lang', targetLang); + // DeepL/DeepLX handling + 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 = canDeeplFree ? this.serverSettings.deeplFreeInstance as string : this.serverSettings.deeplIsPro ? 'https://api.deepl.com/v2/translate' : 'https://api-free.deepl.com/v2/translate'; - 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; + }[]; + }; - 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 (canLibre) { + const res = await this.httpRequestService.send(this.serverSettings.libreTranslateURL as string, { + 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 ?? '', + }), + }); - 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,10 +178,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- }); return { - sourceLang: languageNames.of(json.source_lang), - text: json.data, + sourceLang: languageNames.of(json.detectedLanguage.language as string), + text: json.translatedText, }; } + + return; }); } } 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 <template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template> <MkSpacer :contentMax="700" :marginMin="16" :marginMax="32"> <FormSuspense :p="init"> - <MkFolder> - <template #label>DeepL Translation</template> + <div class="_gaps_m"> + <MkFolder> + <template #label>DeepL Translation</template> - <div class="_gaps_m"> - <MkInput v-model="deeplAuthKey"> - <template #prefix><i class="ti ti-key"></i></template> - <template #label>DeepL Auth Key</template> - </MkInput> - <MkSwitch v-model="deeplIsPro"> - <template #label>Pro account</template> - </MkSwitch> + <div class="_gaps_m"> + <MkInput v-model="deeplAuthKey"> + <template #prefix><i class="ti ti-key"></i></template> + <template #label>DeepL Auth Key</template> + </MkInput> + <MkSwitch v-model="deeplIsPro"> + <template #label>Pro account</template> + </MkSwitch> - <MkSwitch v-model="deeplFreeMode"> - <template #label>{{ i18n.ts.deeplFreeMode }}</template> - </MkSwitch> - <MkInput v-if="deeplFreeMode" v-model="deeplFreeInstance" :placeholder="'example.com/translate'"> - <template #prefix><i class="ph-globe-simple ph-bold ph-lg"></i></template> - <template #label>DeepLX-JS URL</template> - <template #caption>{{ i18n.ts.deeplFreeModeDescription }}</template> - </MkInput> + <MkSwitch v-model="deeplFreeMode"> + <template #label>{{ i18n.ts.deeplFreeMode }}</template> + </MkSwitch> + <MkInput v-if="deeplFreeMode" v-model="deeplFreeInstance" :placeholder="'example.com/translate'"> + <template #prefix><i class="ph-globe-simple ph-bold ph-lg"></i></template> + <template #label>DeepLX-JS URL</template> + <template #caption>{{ i18n.ts.deeplFreeModeDescription }}</template> + </MkInput> - <MkButton primary @click="save_deepl">Save</MkButton> - </div> - </MkFolder> + <MkButton primary @click="save_deepl">Save</MkButton> + </div> + </MkFolder> + + <MkFolder> + <template #label>LibreTranslate Translation</template> + + <div class="_gaps_m"> + <MkInput v-model="libreTranslateURL" :placeholder="'example.com/translate'"> + <template #prefix><i class="ph-globe-simple ph-bold ph-lg"></i></template> + <template #label>LibreTranslate URL</template> + </MkInput> + + <MkInput v-model="libreTranslateKey"> + <template #prefix><i class="ti ti-key"></i></template> + <template #label>LibreTranslate Api Key</template> + </MkInput> + + <MkButton primary @click="save_libre">Save</MkButton> + </div> + </MkFolder> + </div> </FormSuspense> </MkSpacer> </MkStickyContainer> @@ -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<string>(''); +const deeplAuthKey = ref<string | null>(''); const deeplIsPro = ref<boolean>(false); const deeplFreeMode = ref<boolean>(false); -const deeplFreeInstance = ref<string>(''); +const deeplFreeInstance = ref<string | null>(''); +const libreTranslateURL = ref<string | null>(''); +const libreTranslateKey = ref<string | null>(''); 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; |