refactor (backend): separate translate function into another file, use post language info for translations, use deepl-node package

This commit is contained in:
naskya 2024-03-02 00:27:21 +09:00
parent fb12399a52
commit 2414cf3ec7
No known key found for this signature in database
GPG key ID: 712D413B3A9FED5C
5 changed files with 126 additions and 95 deletions

View file

@ -53,6 +53,7 @@
"date-fns": "3.3.1", "date-fns": "3.3.1",
"decompress": "^4.2.1", "decompress": "^4.2.1",
"deep-email-validator": "0.1.21", "deep-email-validator": "0.1.21",
"deepl-node": "1.12.0",
"escape-regexp": "0.0.1", "escape-regexp": "0.0.1",
"feed": "4.2.2", "feed": "4.2.2",
"file-type": "19.0.0", "file-type": "19.0.0",

View file

@ -380,3 +380,4 @@ export const iso639Regional = {
}; };
export const langmap = Object.assign({}, langmapNoRegion, iso639Regional); export const langmap = Object.assign({}, langmapNoRegion, iso639Regional);
export type PostLanguage = keyof typeof langmap;

View file

@ -0,0 +1,88 @@
import fetch from "node-fetch";
import { Converter } from "opencc-js";
import { getAgentByUrl } from "@/misc/fetch.js";
import { fetchMeta } from "@/misc/fetch-meta.js";
import type { PostLanguage } from "@/misc/langmap";
import * as deepl from "deepl-node";
function convertChinese(convert: boolean, src: string) {
if (!convert) return src;
const converter = Converter({ from: "cn", to: "twp" });
return converter(src);
}
function stem(lang: PostLanguage): string {
let toReturn = lang as string;
if (toReturn.includes("-")) toReturn = toReturn.split("-")[0];
if (toReturn.includes("_")) toReturn = toReturn.split("_")[0];
return toReturn;
}
export async function translate(
text: string,
from: PostLanguage | null,
to: PostLanguage,
) {
const instance = await fetchMeta();
if (instance.deeplAuthKey == null && instance.libreTranslateApiUrl == null) {
throw Error("No translator is set up on this server.");
}
const source = from == null ? null : stem(from);
const target = stem(to);
if (instance.libreTranslateApiUrl != null) {
const jsonBody = {
q: text,
source: source ?? "auto",
target,
format: "text",
api_key: instance.libreTranslateApiKey ?? "",
};
const url = new URL(instance.libreTranslateApiUrl);
if (url.pathname.endsWith("/")) {
url.pathname = url.pathname.slice(0, -1);
}
if (!url.pathname.endsWith("/translate")) {
url.pathname += "/translate";
}
const res = await fetch(url.toString(), {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(jsonBody),
agent: getAgentByUrl,
});
const json = (await res.json()) as {
detectedLanguage?: {
confidence: number;
language: string;
};
translatedText: string;
};
return {
sourceLang: source ?? json.detectedLanguage?.language,
text: convertChinese(
["zh-hant", "zh-TW"].includes(to),
json.translatedText,
),
};
}
const deeplTranslator = new deepl.Translator(instance.deeplAuthKey ?? "");
const result = await deeplTranslator.translateText(
text,
source as deepl.SourceLanguageCode | null,
(target === "en" ? to : target) as deepl.TargetLanguageCode,
);
return {
sourceLang: source ?? result.detectedSourceLang,
text: convertChinese(["zh-hant", "zh-TW"].includes(to), result.text),
};
}

View file

@ -1,11 +1,7 @@
import { URLSearchParams } from "node:url";
import fetch from "node-fetch";
import config from "@/config/index.js";
import { Converter } from "opencc-js";
import { getAgentByUrl } from "@/misc/fetch.js";
import { fetchMeta } from "@/misc/fetch-meta.js";
import { ApiError } from "@/server/api/error.js"; import { ApiError } from "@/server/api/error.js";
import { getNote } from "@/server/api/common/getters.js"; import { getNote } from "@/server/api/common/getters.js";
import { translate } from "@/misc/translate.js";
import type { PostLanguage } from "@/misc/langmap.js";
import define from "@/server/api/define.js"; import define from "@/server/api/define.js";
export const meta = { export const meta = {
@ -38,13 +34,6 @@ export const paramDef = {
required: ["noteId", "targetLang"], required: ["noteId", "targetLang"],
} as const; } as const;
function convertChinese(convert: boolean, src: string) {
if (!convert) return src;
const converter = Converter({ from: "cn", to: "twp" });
return converter(src);
}
export default define(meta, paramDef, async (ps, user) => { export default define(meta, paramDef, async (ps, user) => {
const note = await getNote(ps.noteId, user).catch((err) => { const note = await getNote(ps.noteId, user).catch((err) => {
if (err.id === "9725d0ce-ba28-4dde-95a7-2cbb2c15de24") if (err.id === "9725d0ce-ba28-4dde-95a7-2cbb2c15de24")
@ -56,86 +45,9 @@ export default define(meta, paramDef, async (ps, user) => {
return 204; return 204;
} }
const instance = await fetchMeta(); return translate(
note.text,
if (instance.deeplAuthKey == null && instance.libreTranslateApiUrl == null) { note.lang as PostLanguage | null,
return 204; // TODO: 良い感じのエラー返す ps.targetLang as PostLanguage,
} );
let targetLang = ps.targetLang;
if (targetLang.includes("-")) targetLang = targetLang.split("-")[0];
if (targetLang.includes("_")) targetLang = targetLang.split("_")[0];
if (instance.libreTranslateApiUrl != null) {
const jsonBody = {
q: note.text,
source: "auto",
target: targetLang,
format: "text",
api_key: instance.libreTranslateApiKey ?? "",
};
const url = new URL(instance.libreTranslateApiUrl);
if (url.pathname.endsWith("/")) {
url.pathname = url.pathname.slice(0, -1);
}
if (!url.pathname.endsWith("/translate")) {
url.pathname += "/translate";
}
const res = await fetch(url.toString(), {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(jsonBody),
agent: getAgentByUrl,
});
const json = (await res.json()) as {
detectedLanguage?: {
confidence: number;
language: string;
};
translatedText: string;
};
return {
sourceLang: json.detectedLanguage?.language,
text: convertChinese(ps.targetLang === "zh-TW", json.translatedText),
};
}
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 json = (await res.json()) as {
translations: {
detected_source_language: string;
text: string;
}[];
};
return {
sourceLang: json.translations[0].detected_source_language,
text: convertChinese(ps.targetLang === "zh-TW", json.translations[0].text),
};
}); });

View file

@ -150,6 +150,9 @@ importers:
deep-email-validator: deep-email-validator:
specifier: 0.1.21 specifier: 0.1.21
version: 0.1.21 version: 0.1.21
deepl-node:
specifier: 1.12.0
version: 1.12.0
escape-regexp: escape-regexp:
specifier: 0.0.1 specifier: 0.0.1
version: 0.0.1 version: 0.0.1
@ -7430,6 +7433,18 @@ packages:
resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
dev: true dev: true
/deepl-node@1.12.0:
resolution: {integrity: sha512-c/8x1R0dXPL7NSDdQ94lYPou/A+I6cbo6b7gFb/28HbjcHnKB4RtWXWLgdv7n51GEXL7OE2eoRZQcAu4ZI+vGg==}
engines: {node: '>=12.0'}
dependencies:
'@types/node': 20.11.21
axios: 1.6.7
form-data: 3.0.1
loglevel: 1.9.1
transitivePeerDependencies:
- debug
dev: false
/deepmerge@4.3.1: /deepmerge@4.3.1:
resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
@ -9416,6 +9431,15 @@ packages:
engines: {node: '>= 18'} engines: {node: '>= 18'}
dev: false dev: false
/form-data@3.0.1:
resolution: {integrity: sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==}
engines: {node: '>= 6'}
dependencies:
asynckit: 0.4.0
combined-stream: 1.0.8
mime-types: 2.1.35
dev: false
/form-data@4.0.0: /form-data@4.0.0:
resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==} resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==}
engines: {node: '>= 6'} engines: {node: '>= 6'}
@ -12254,6 +12278,11 @@ packages:
is-unicode-supported: 0.1.0 is-unicode-supported: 0.1.0
dev: true dev: true
/loglevel@1.9.1:
resolution: {integrity: sha512-hP3I3kCrDIMuRwAwHltphhDM1r8i55H33GgqjXbrisuJhF4kRhW1dNuxsRklp4bXl8DSdLaNLuiL4A/LWRfxvg==}
engines: {node: '>= 0.6.0'}
dev: false
/lowercase-keys@2.0.0: /lowercase-keys@2.0.0:
resolution: {integrity: sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==} resolution: {integrity: sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==}
engines: {node: '>=8'} engines: {node: '>=8'}