diff --git a/package.json b/package.json index 9b26c06b1c..25d22275a0 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "cleanall": "npm run clean-all" }, "dependencies": { + "@vitalets/google-translate-api": "8.0.0", "execa": "5.1.1", "gulp": "4.0.2", "gulp-cssnano": "2.1.3", diff --git a/packages/backend/migration/1660183643857-multipleTranslationServices.js b/packages/backend/migration/1660183643857-multipleTranslationServices.js new file mode 100644 index 0000000000..9f2b6ce725 --- /dev/null +++ b/packages/backend/migration/1660183643857-multipleTranslationServices.js @@ -0,0 +1,17 @@ +export class multipleTranslationServices1660183643857 { + name = 'multipleTranslationServices1660183643857' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" ADD "translatorType" character varying(32)`); + await queryRunner.query('SELECT "deeplAuthKey" FROM "meta" where "deeplAuthKey" is not null') + .then(deeplAuthKey => { + if (deeplAuthKey.length > 0) { + return queryRunner.query('UPDATE "meta" SET "translatorType" = "DeepL"'); + } + }) + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "translatorType"`); + } +} diff --git a/packages/backend/src/models/entities/meta.ts b/packages/backend/src/models/entities/meta.ts index ac606b3315..c3dfe079d8 100644 --- a/packages/backend/src/models/entities/meta.ts +++ b/packages/backend/src/models/entities/meta.ts @@ -335,6 +335,12 @@ export class Meta { }) public discordClientSecret: string | null; + @Column('varchar', { + length: 32, + nullable: true, + }) + public translatorType: string | null; + @Column('varchar', { length: 128, 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 8746119687..e170e47709 100644 --- a/packages/backend/src/server/api/endpoints/admin/meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/meta.ts @@ -155,6 +155,10 @@ export const meta = { type: 'boolean', optional: false, nullable: false, }, + translatorType: { + type: 'string', + optional: false, nullable: true, + }, proxyAccountName: { type: 'string', optional: false, nullable: true, @@ -380,7 +384,9 @@ export default define(meta, paramDef, async (ps, me) => { enableGithubIntegration: instance.enableGithubIntegration, enableDiscordIntegration: instance.enableDiscordIntegration, enableServiceWorker: instance.enableServiceWorker, - translatorAvailable: instance.deeplAuthKey != null, + // translatorAvailable: instance.deeplAuthKey != null, + translatorAvailable: instance.translatorType != null, + translatorType: instance.translatorType, pinnedPages: instance.pinnedPages, pinnedClipId: instance.pinnedClipId, cacheRemoteFiles: instance.cacheRemoteFiles, 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 f14aa41050..abba855390 100644 --- a/packages/backend/src/server/api/endpoints/admin/update-meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/update-meta.ts @@ -63,6 +63,7 @@ export const paramDef = { type: 'string', } }, summalyProxy: { type: 'string', nullable: true }, + translatorType: { type: 'string', nullable: true }, deeplAuthKey: { type: 'string', nullable: true }, deeplIsPro: { type: 'boolean' }, enableTwitterIntegration: { type: 'boolean' }, @@ -406,6 +407,14 @@ export default define(meta, paramDef, async (ps, me) => { set.objectStorageS3ForcePathStyle = ps.objectStorageS3ForcePathStyle; } + if (ps.translatorType !== undefined) { + if (ps.translatorType === '') { + set.translatorType = null; + } else { + set.translatorType = ps.translatorType; + } + } + if (ps.deeplAuthKey !== undefined) { if (ps.deeplAuthKey === '') { set.deeplAuthKey = null; diff --git a/packages/backend/src/server/api/endpoints/meta.ts b/packages/backend/src/server/api/endpoints/meta.ts index 5b624842c3..5e52c6dbf8 100644 --- a/packages/backend/src/server/api/endpoints/meta.ts +++ b/packages/backend/src/server/api/endpoints/meta.ts @@ -377,7 +377,8 @@ export default define(meta, paramDef, async (ps, me) => { enableServiceWorker: instance.enableServiceWorker, - translatorAvailable: instance.deeplAuthKey != null, + // translatorAvailable: instance.deeplAuthKey != null, + translatorAvailable: instance.translatorType != null, ...(ps.detail ? { pinnedPages: instance.pinnedPages, diff --git a/packages/backend/src/server/api/endpoints/notes/translate.ts b/packages/backend/src/server/api/endpoints/notes/translate.ts index 5e40e7106f..6000e3fe4d 100644 --- a/packages/backend/src/server/api/endpoints/notes/translate.ts +++ b/packages/backend/src/server/api/endpoints/notes/translate.ts @@ -1,4 +1,5 @@ import { URLSearchParams } from 'node:url'; +import googletr from '@vitalets/google-translate-api'; import fetch from 'node-fetch'; import config from '@/config/index.js'; import { getAgentByUrl } from '@/misc/fetch.js'; @@ -24,6 +25,11 @@ export const meta = { code: 'NO_SUCH_NOTE', id: 'bea9b03f-36e0-49c5-a4db-627a029f8971', }, + noTranslateService: { + message: 'Translate service is not available.', + code: 'NO_TRANSLATE_SERVICE', + id: 'bef6e895-c05d-4499-9815-035ed18b0e31', + }, }, } as const; @@ -42,6 +48,10 @@ export default define(meta, paramDef, async (ps, user) => { if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote); throw e; }); + const translatorServices = [ + 'DeepL', + 'GoogleNoAPI', + ]; if (!(await Notes.isVisibleForMe(note, user ? user.id : null))) { return 204; // TODO: 良い感じのエラー返す @@ -53,42 +63,62 @@ export default define(meta, paramDef, async (ps, user) => { const instance = await fetchMeta(); - if (instance.deeplAuthKey == null) { - return 204; // TODO: 良い感じのエラー返す + if (instance.translatorType == null || !translatorServices.includes(instance.translatorType)) { + throw new ApiError(meta.errors.noTranslateService); } - let targetLang = ps.targetLang; - if (targetLang.includes('-')) targetLang = targetLang.split('-')[0]; + if (instance.translatorType === 'DeepL') { + if (instance.deeplAuthKey == null) { + return 204; // TODO: 良い感じのエラー返す + } + + let targetLang = ps.targetLang; + if (targetLang.includes('-')) targetLang = targetLang.split('-')[0]; + + const params = new URLSearchParams(); + params.append('auth_key', instance.deeplAuthKey); + params.append('text', note.text); + params.append('target_lang', targetLang); + + const endpoint = instance.deeplIsPro ? 'https://api.deepl.com/v2/translate' : 'https://api-free.deepl.com/v2/translate'; + + const res = await fetch(endpoint, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + 'User-Agent': config.userAgent, + Accept: 'application/json, */*', + }, + body: params, + // TODO + //timeout: 10000, + agent: getAgentByUrl, + }); - const params = new URLSearchParams(); - params.append('auth_key', instance.deeplAuthKey); - params.append('text', note.text); - params.append('target_lang', targetLang); + const json = (await res.json()) as { + translations: { + detected_source_language: string; + text: string; + }[]; + }; - const endpoint = instance.deeplIsPro ? 'https://api.deepl.com/v2/translate' : 'https://api-free.deepl.com/v2/translate'; + return { + sourceLang: json.translations[0].detected_source_language, + text: json.translations[0].text, + translator: translatorServices, + }; + } else if (instance.translatorType === 'GoogleNoAPI') { + let targetLang = ps.targetLang; + if (targetLang.includes('-')) targetLang = targetLang.split('-')[0]; + + const json = await googletr(note.text, { to: targetLang }); - const res = await fetch(endpoint, { - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - 'User-Agent': config.userAgent, - Accept: 'application/json, */*', - }, - body: params, - // TODO - //timeout: 10000, - agent: getAgentByUrl, - }); + return { + sourceLang: json.from.language.iso, + text: json.text, + translator: translatorServices, + }; + } - 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, - }; + return 204; // TODO: 良い感じのエラー返す }); diff --git a/packages/client/src/pages/admin/settings.vue b/packages/client/src/pages/admin/settings.vue index 496eb46ea4..3824bd7c52 100644 --- a/packages/client/src/pages/admin/settings.vue +++ b/packages/client/src/pages/admin/settings.vue @@ -130,15 +130,24 @@ - + - - - - - - - + + + + + + + + @@ -157,6 +166,7 @@ import FormInfo from '@/components/ui/info.vue'; import FormSection from '@/components/form/section.vue'; import FormSplit from '@/components/form/split.vue'; import FormSuspense from '@/components/form/suspense.vue'; +import FormRadios from '@/components/form/radios.vue' import * as os from '@/os'; import { fetchInstance } from '@/instance'; import { i18n } from '@/i18n'; @@ -184,6 +194,7 @@ let emailRequiredForSignup: boolean = $ref(false); let enableServiceWorker: boolean = $ref(false); let swPublicKey: any = $ref(null); let swPrivateKey: any = $ref(null); +let translatorType: string | null = $ref(null); let deeplAuthKey: string = $ref(''); let deeplIsPro: boolean = $ref(false); @@ -211,6 +222,7 @@ async function init() { enableServiceWorker = meta.enableServiceWorker; swPublicKey = meta.swPublickey; swPrivateKey = meta.swPrivateKey; + translatorType = meta.translatorType; deeplAuthKey = meta.deeplAuthKey; deeplIsPro = meta.deeplIsPro; } @@ -239,6 +251,7 @@ function save() { enableServiceWorker, swPublicKey, swPrivateKey, + translatorType, deeplAuthKey, deeplIsPro, }).then(() => {