Skip to content

Commit

Permalink
Add (Cloud Translation - Advanced) Support
Browse files Browse the repository at this point in the history
  • Loading branch information
caipira113 committed Jun 1, 2023
1 parent f7d4191 commit 3c582dd
Show file tree
Hide file tree
Showing 13 changed files with 808 additions and 51 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
export class MultipleTranslationServices1685378242713 {
name = 'MultipleTranslationServices1685378242713'

async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "meta" ADD "translatorType" character varying(32)`);
await queryRunner.query(`ALTER TABLE "meta" ADD "ctav3SaKey" character varying(5120)`);
await queryRunner.query(`ALTER TABLE "meta" ADD "ctav3ProjectId" character varying(1024)`);
await queryRunner.query(`ALTER TABLE "meta" ADD "ctav3Location" character varying(1024)`);
await queryRunner.query(`ALTER TABLE "meta" ADD "ctav3Model" character varying(1024)`);
await queryRunner.query(`ALTER TABLE "meta" ADD "ctav3Glossary" character varying(1024)`);
}

async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "translatorType"`);
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "ctav3SaKey"`);
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "ctav3ProjectId"`);
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "ctav3Location"`);
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "ctav3Model"`);
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "ctav3Glossary"`);
}

}
1 change: 1 addition & 0 deletions packages/backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@
"@fastify/multipart": "7.6.0",
"@fastify/static": "6.10.1",
"@fastify/view": "7.4.1",
"@google-cloud/translate": "^7.2.1",
"@nestjs/common": "9.4.2",
"@nestjs/core": "9.4.2",
"@nestjs/testing": "9.4.2",
Expand Down
36 changes: 36 additions & 0 deletions packages/backend/src/models/entities/Meta.ts
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,12 @@ export class Meta {
})
public swPrivateKey: string | null;

@Column('varchar', {
length: 32,
nullable: true,
})
public translatorType: string | null;

@Column('varchar', {
length: 1024,
nullable: true,
Expand All @@ -277,6 +283,36 @@ export class Meta {
})
public deeplIsPro: boolean;

@Column('varchar', {
length: 5120,
nullable: true,
})
public ctav3SaKey: string | null;

@Column('varchar', {
length: 1024,
nullable: true,
})
public ctav3ProjectId: string | null;

@Column('varchar', {
length: 1024,
nullable: true,
})
public ctav3Location: string | null;

@Column('varchar', {
length: 1024,
nullable: true,
})
public ctav3Model: string | null;

@Column('varchar', {
length: 1024,
nullable: true,
})
public ctav3Glossary: string | null;

@Column('varchar', {
length: 1024,
nullable: true,
Expand Down
8 changes: 7 additions & 1 deletion packages/backend/src/server/api/endpoints/admin/meta.ts
Original file line number Diff line number Diff line change
Expand Up @@ -313,7 +313,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
defaultDarkTheme: instance.defaultDarkTheme,
enableEmail: instance.enableEmail,
enableServiceWorker: instance.enableServiceWorker,
translatorAvailable: instance.deeplAuthKey != null,
translatorAvailable: instance.translatorType != null,
cacheRemoteFiles: instance.cacheRemoteFiles,
pinnedUsers: instance.pinnedUsers,
hiddenTags: instance.hiddenTags,
Expand Down Expand Up @@ -349,8 +349,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
objectStorageUseProxy: instance.objectStorageUseProxy,
objectStorageSetPublicRead: instance.objectStorageSetPublicRead,
objectStorageS3ForcePathStyle: instance.objectStorageS3ForcePathStyle,
translatorType: instance.translatorType,
deeplAuthKey: instance.deeplAuthKey,
deeplIsPro: instance.deeplIsPro,
ctav3SaKey: instance.ctav3SaKey,
ctav3ProjectId: instance.ctav3ProjectId,
ctav3Location: instance.ctav3Location,
ctav3Model: instance.ctav3Model,
ctav3Glossary: instance.ctav3Glossary,
enableIpLogging: instance.enableIpLogging,
enableActiveEmailValidation: instance.enableActiveEmailValidation,
enableChartsForRemoteUser: instance.enableChartsForRemoteUser,
Expand Down
30 changes: 30 additions & 0 deletions packages/backend/src/server/api/endpoints/admin/update-meta.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,14 @@ export const paramDef = {
type: 'string',
} },
summalyProxy: { type: 'string', nullable: true },
translatorType: { type: 'string', nullable: true },
deeplAuthKey: { type: 'string', nullable: true },
deeplIsPro: { type: 'boolean' },
ctav3SaKey: { type: 'string', nullable: true },
ctav3ProjectId: { type: 'string', nullable: true },
ctav3Location: { type: 'string', nullable: true },
ctav3Model: { type: 'string', nullable: true },
ctav3Glossary: { type: 'string', nullable: true },
enableEmail: { type: 'boolean' },
email: { type: 'string', nullable: true },
smtpSecure: { type: 'boolean' },
Expand Down Expand Up @@ -361,6 +367,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
set.objectStorageS3ForcePathStyle = ps.objectStorageS3ForcePathStyle;
}

if (ps.translatorType !== undefined) {
set.translatorType = ps.translatorType;
}

if (ps.deeplAuthKey !== undefined) {
if (ps.deeplAuthKey === '') {
set.deeplAuthKey = null;
Expand All @@ -373,6 +383,26 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
set.deeplIsPro = ps.deeplIsPro;
}

if (ps.ctav3SaKey !== undefined) {
set.ctav3SaKey = ps.ctav3SaKey;
}

if (ps.ctav3ProjectId !== undefined) {
set.ctav3ProjectId = ps.ctav3ProjectId;
}

if (ps.ctav3Location !== undefined) {
set.ctav3Location = ps.ctav3Location;
}

if (ps.ctav3Model !== undefined) {
set.ctav3Model = ps.ctav3Model;
}

if (ps.ctav3Glossary !== undefined) {
set.ctav3Glossary = ps.ctav3Glossary;
}

if (ps.enableIpLogging !== undefined) {
set.enableIpLogging = ps.enableIpLogging;
}
Expand Down
2 changes: 1 addition & 1 deletion packages/backend/src/server/api/endpoints/meta.ts
Original file line number Diff line number Diff line change
Expand Up @@ -306,7 +306,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
enableEmail: instance.enableEmail,
enableServiceWorker: instance.enableServiceWorker,

translatorAvailable: instance.deeplAuthKey != null,
translatorAvailable: instance.translatorType != null,

serverRules: instance.serverRules,

Expand Down
139 changes: 113 additions & 26 deletions packages/backend/src/server/api/endpoints/notes/translate.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { URLSearchParams } from 'node:url';
import fs from 'node:fs';
import { Inject, Injectable } from '@nestjs/common';
import { TranslationServiceClient } from '@google-cloud/translate';
import type { NotesRepository } from '@/models/index.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { Config } from '@/config.js';
Expand All @@ -8,6 +10,7 @@ import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { MetaService } from '@/core/MetaService.js';
import { HttpRequestService } from '@/core/HttpRequestService.js';
import { GetterService } from '@/server/api/GetterService.js';
import { createTemp } from '@/misc/create-temp.js';
import { ApiError } from '../../error.js';

export const meta = {
Expand All @@ -26,6 +29,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;

Expand Down Expand Up @@ -69,40 +77,119 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {

const instance = await this.metaService.fetch();

if (instance.deeplAuthKey == null) {
return 204; // TODO: 良い感じのエラー返す
}
const translatorServices = [
'deepl',
'ctav3',
];

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];

const params = new URLSearchParams();
params.append('auth_key', instance.deeplAuthKey);
params.append('text', note.text);
params.append('target_lang', targetLang);
let translationResult;
if (instance.translatorType === 'deepl') {
if (instance.deeplAuthKey == null) {
return 204; // TODO: 良い感じのエラー返す
}
translationResult = await this.translateDeepL(note.text, targetLang, instance.deeplAuthKey, instance.deeplIsPro, instance.translatorType);
} else if (instance.translatorType === 'ctav3') {
if (instance.ctav3SaKey == null) { return 204; } else if (instance.ctav3ProjectId == null) { return 204; }
else if (instance.ctav3Location == null) { return 204; }
translationResult = await this.apiCloudTranslationAdvanced(
note.text, targetLang, instance.ctav3SaKey, instance.ctav3ProjectId, instance.ctav3Location, instance.ctav3Model, instance.ctav3Glossary, instance.translatorType,
);
} else {
throw new Error('Unsupported translator type');
}

return {
sourceLang: translationResult.sourceLang,
text: translationResult.text,
translator: translationResult.translator,
};
});
}

private async translateDeepL(text: string, targetLang: string, authKey: string, isPro: boolean, provider: string) {
const params = new URLSearchParams();
params.append('auth_key', authKey);
params.append('text', text);
params.append('target_lang', targetLang);

const endpoint = isPro ? '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(),
});

const endpoint = instance.deeplIsPro ? 'https://api.deepl.com/v2/translate' : 'https://api-free.deepl.com/v2/translate';
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,
translator: provider,
};
}

const res = await this.httpRequestService.send(endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
Accept: 'application/json, */*',
},
body: params.toString(),
});
private async apiCloudTranslationAdvanced(text: string, targetLang: string, saKey: string, projectId: string, location: string, model: string | null, glossary: string | null, provider: string) {
const [path, cleanup] = await createTemp();
fs.writeFileSync(path, saKey);
process.env.GOOGLE_APPLICATION_CREDENTIALS = path;

const json = (await res.json()) as {
translations: {
detected_source_language: string;
text: string;
}[];
};
const translationClient = new TranslationServiceClient();

return {
sourceLang: json.translations[0].detected_source_language,
text: json.translations[0].text,
const detectRequest = {
parent: `projects/${projectId}/locations/${location}`,
content: text,
};

let detectedLanguage = null;
let glossaryConfig = null;
if (glossary !== '' && glossary !== null) {
glossaryConfig = {
glossary: `projects/${projectId}/locations/${location}/glossaries/${glossary}`,
};
});
const [detectResponse] = await translationClient.detectLanguage(detectRequest);
detectedLanguage = detectResponse.languages && detectResponse.languages[0]?.languageCode;
}

let modelConfig = null;
if (model !== '' && model !== null) {
modelConfig = `projects/${projectId}/locations/${location}/models/${model}`;
}

const translateRequest = {
parent: `projects/${projectId}/locations/${location}`,
contents: [text],
mimeType: 'text/plain',
sourceLanguageCode: null,
targetLanguageCode: detectedLanguage !== null ? detectedLanguage : targetLang,
model: modelConfig,
glossaryConfig: glossaryConfig,
};
const [translateResponse] = await translationClient.translateText(translateRequest);
const translatedText = translateResponse.translations && translateResponse.translations[0]?.translatedText;
const detectedLanguageCode = translateResponse.translations && translateResponse.translations[0]?.detectedLanguageCode;

delete process.env.GOOGLE_APPLICATION_CREDENTIALS;
cleanup();
return {
sourceLang: detectedLanguage !== null ? detectedLanguage : detectedLanguageCode,
text: translatedText,
translator: provider,
};
}
}
22 changes: 22 additions & 0 deletions packages/frontend/assets/color-short.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading

0 comments on commit 3c582dd

Please sign in to comment.