forked from naskya/firefish
refactor: separate translate function into another file & use deepl-node package
This commit is contained in:
parent
11541b1e63
commit
7f056e729f
6 changed files with 133 additions and 96 deletions
|
@ -38,6 +38,7 @@
|
|||
|
||||
## 細かい変更点
|
||||
|
||||
- 翻訳機能にて、投稿言語が指定されていない場合にのみ言語の自動検出を用いるように変更
|
||||
- アップデート時に更新内容を確認できる機能を追加
|
||||
- 英語のメッセージしか出ないけど、日本語に翻訳する機能も欲しいですか?
|
||||
- 依存ライブラリを最新版にアップデート
|
||||
|
|
|
@ -189,6 +189,9 @@ importers:
|
|||
deep-email-validator:
|
||||
specifier: 0.1.21
|
||||
version: 0.1.21
|
||||
deepl-node:
|
||||
specifier: 1.11.0
|
||||
version: 1.11.0
|
||||
escape-regexp:
|
||||
specifier: 0.0.1
|
||||
version: 0.0.1
|
||||
|
@ -7283,6 +7286,18 @@ packages:
|
|||
resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
|
||||
dev: true
|
||||
|
||||
/deepl-node@1.11.0:
|
||||
resolution: {integrity: sha512-PLz38WIfWzVScbz4S+32vI9yCUAXy3cpkKSMPC+IDpvrwKrQ2TSzwkoBh7CJOkPjaX8ZMvjwAc+C5eTCPamQTg==}
|
||||
engines: {node: '>=12.0'}
|
||||
dependencies:
|
||||
'@types/node': 20.10.5
|
||||
axios: 1.6.3
|
||||
form-data: 3.0.1
|
||||
loglevel: 1.8.1
|
||||
transitivePeerDependencies:
|
||||
- debug
|
||||
dev: false
|
||||
|
||||
/deepmerge@4.3.1:
|
||||
resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
@ -9231,6 +9246,15 @@ packages:
|
|||
mime-types: 2.1.35
|
||||
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:
|
||||
resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==}
|
||||
engines: {node: '>= 6'}
|
||||
|
@ -12093,6 +12117,11 @@ packages:
|
|||
is-unicode-supported: 0.1.0
|
||||
dev: true
|
||||
|
||||
/loglevel@1.8.1:
|
||||
resolution: {integrity: sha512-tCRIJM51SHjAayKwC+QAg8hT8vg6z7GSgLJKGvzuPb1Wc+hLzqtuVLxp6/HzSPOozuK+8ErAhy7U/sVzw8Dgfg==}
|
||||
engines: {node: '>= 0.6.0'}
|
||||
dev: false
|
||||
|
||||
/long@4.0.0:
|
||||
resolution: {integrity: sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==}
|
||||
dev: false
|
||||
|
|
|
@ -57,6 +57,7 @@
|
|||
"date-fns": "3.0.6",
|
||||
"decompress": "4.2.1",
|
||||
"deep-email-validator": "0.1.21",
|
||||
"deepl-node": "1.11.0",
|
||||
"escape-regexp": "0.0.1",
|
||||
"feed": "4.2.2",
|
||||
"file-type": "18.7.0",
|
||||
|
|
|
@ -379,3 +379,4 @@ export const iso639Regional = {
|
|||
};
|
||||
|
||||
export const langmap = Object.assign({}, langmapNoRegion, iso639Regional);
|
||||
export type Language = keyof typeof langmap;
|
||||
|
|
88
packages/backend/src/misc/translate.ts
Normal file
88
packages/backend/src/misc/translate.ts
Normal 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 { Language } 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: Language): string {
|
||||
let toReturn = lang as string;
|
||||
if (toReturn.includes("-")) toReturn = toReturn.split("-")[0];
|
||||
if (toReturn.includes("_")) toReturn = toReturn.split("_")[0];
|
||||
return toReturn;
|
||||
}
|
||||
|
||||
export default async function (
|
||||
text: string,
|
||||
from: Language | null,
|
||||
to: Language,
|
||||
) {
|
||||
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 as deepl.TargetLanguageCode,
|
||||
);
|
||||
|
||||
return {
|
||||
sourceLang: source ?? result.detectedSourceLang,
|
||||
text: convertChinese(["zh-hant", "zh-TW"].includes(to), result.text),
|
||||
};
|
||||
}
|
|
@ -1,12 +1,8 @@
|
|||
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 { getNote } from "@/server/api/common/getters.js";
|
||||
import define from "@/server/api/define.js";
|
||||
import translate from "@/misc/translate.js";
|
||||
import type { Language } from "@/misc/langmap";
|
||||
|
||||
export const meta = {
|
||||
tags: ["notes"],
|
||||
|
@ -26,6 +22,11 @@ export const meta = {
|
|||
code: "NO_SUCH_NOTE",
|
||||
id: "bea9b03f-36e0-49c5-a4db-627a029f8971",
|
||||
},
|
||||
noteTextIsNull: {
|
||||
message: "The text of this note is null.",
|
||||
code: "NOTE_TEXT_IS_NULL",
|
||||
id: "c2794117-1a8d-4fe5-8925-0eca24ba47d0",
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
|
@ -38,13 +39,6 @@ export const paramDef = {
|
|||
required: ["noteId", "targetLang"],
|
||||
} 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) => {
|
||||
const note = await getNote(ps.noteId, user).catch((err) => {
|
||||
if (err.id === "9725d0ce-ba28-4dde-95a7-2cbb2c15de24")
|
||||
|
@ -53,89 +47,12 @@ export default define(meta, paramDef, async (ps, user) => {
|
|||
});
|
||||
|
||||
if (note.text == null) {
|
||||
return 204;
|
||||
throw new ApiError(meta.errors.noteTextIsNull);
|
||||
}
|
||||
|
||||
const instance = await fetchMeta();
|
||||
|
||||
if (instance.deeplAuthKey == null && instance.libreTranslateApiUrl == null) {
|
||||
return 204; // TODO: 良い感じのエラー返す
|
||||
}
|
||||
|
||||
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),
|
||||
};
|
||||
return translate(
|
||||
note.text,
|
||||
note.lang as Language | null,
|
||||
ps.targetLang as Language,
|
||||
);
|
||||
});
|
||||
|
|
Loading…
Reference in a new issue