-
-
Notifications
You must be signed in to change notification settings - Fork 12
/
Copy pathtranslate.openai.mjs
103 lines (91 loc) · 3.26 KB
/
translate.openai.mjs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
import { OpenAI } from 'openai';
import { HttpsProxyAgent } from 'https-proxy-agent';
import picocolors from 'picocolors';
const model = 'gpt-4o';
/** @type {typeof console.log} */
export const log = (...args) => {
console.log(picocolors.dim(new Date().toLocaleTimeString()), ...args);
}
/**
* Instructions for the translator.
*
* @param {string[]} terms Terms to be added to the instructions.
*/
const buildInstructions = (terms) => [
'Keep all frontmatter keys in the original language, translate title and description.',
'Do not translate JSON keys, inline code, component names and keys.',
'For mermaid diagrams, translate the text inside the diagram, but keep the diagram type and structure.',
'Do not translate the following nouns: OAuth, OpenID Connect.',
`Whenever seeing one of the following terms, including in title, you must add the untranslated term in parentheses after the translated term, e.g. "认证 (Authentication)": ${terms.join(', ')}.`,
'Prefer using "你" instead of "您" in Chinese.',
'Make sure there is a space between the CJK and non-CJK characters.',
'Respond with the translated content only.',
];
export class OpenAiTranslate {
httpProxy = process.env.HTTP_PROXY;
openai = new OpenAI({
httpAgent: this.httpProxy ? new HttpsProxyAgent(this.httpProxy) : undefined,
});
/** @type {string} */
instructions;
/** @param {string[]} terms to be added to the instructions. */
constructor(terms) {
if (this.httpProxy) {
log(`Using HTTP proxy: ${this.httpProxy}`);
}
if (!Array.isArray(terms)) {
throw new TypeError('Expected `terms` to be an array');
}
this.instructions = buildInstructions(terms).map((instruction) => `- ${instruction}`).join('\n');
}
/**
* @param {string} content The content to translate.
* @param {string} target The target locale.
* @param {import('listr2').ListrTaskWrapper} task The task wrapper to update the task status.
* @returns {Promise<string>} Translated content.
*/
async translate(content, target, task) {
const stream = await this.openai.chat.completions.create({
model,
messages: [
{
role: 'system',
content: [
`You are a assistant translator and will receive a MDX file. Translate it to locale "${target}". Detailed instructions are as follows:`,
]
.concat(this.instructions)
.join('\n'),
},
{
role: 'user',
content: content,
},
],
temperature: 0.1,
stream: true,
});
if (task) {
task.output = 'Waiting for response...';
} else {
log('Waiting for response...');
}
// Extract the translated content from the stream.
let count = 0;
let result = '';
for await (const chunk of stream) {
const choice0 = chunk.choices[0];
result += choice0?.delta.content ?? '';
if (task) {
task.output = `Receiving response (${++count} chunks)`;
} else {
log(`Receiving response (${++count} chunks)`);
}
if (choice0?.finish_reason && choice0.finish_reason !== 'stop') {
throw new Error(
`Invalid response from OpenAI, expected \`stop\` but got \`${choice0.finish_reason}\``
);
}
}
return result;
}
}