Skip to content

Commit 95dab5f

Browse files
authored
docs: add translating option with OpenAI (#109)
1 parent c9bdb8e commit 95dab5f

File tree

10 files changed

+572
-120
lines changed

10 files changed

+572
-120
lines changed

docs/README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,7 @@ just docs-gen reference --lang=ko
2424

2525
# Generate specific reference documentations with glob pattern.
2626
just docs-gen reference --pattern='Tree/Methods/*'
27+
28+
# Translate with OpenAI. Or pass api token with environment variables. (/docs/.env)
29+
just docs-gen reference --translate-ai-token="<OpenAI API Key>"
2730
```

docs/gen/cli.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
1+
import path from 'node:path';
2+
import { fileURLToPath } from 'node:url';
13
import { Cli } from 'clipanion';
4+
import dotenv from 'dotenv';
25
import { ReferenceCommand } from './commands/reference';
36

7+
const dirname = path.dirname(fileURLToPath(import.meta.url));
8+
dotenv.config({ path: path.join(dirname, '..', '.env') });
9+
410
const [node, app, ...args] = process.argv;
511

612
const cli = new Cli({

docs/gen/commands/reference.ts

Lines changed: 24 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,30 @@
11
import fs from 'node:fs/promises';
22
import path from 'node:path';
33
import { Command, Option } from 'clipanion';
4-
import { isNotNil } from 'es-toolkit';
54
import micromatch from 'micromatch';
5+
import OpenAI from 'openai';
66
import type { DeclarationReflection } from 'typedoc';
7+
import { type ReferenceDoc, renderReferenceDoc } from '../doc';
78
import { findRootDir } from '../fs';
89
import { LanguageOption } from '../lang';
9-
import {
10-
findCategory,
11-
genErrors,
12-
genExample,
13-
genParameters,
14-
genReturns,
15-
genSignature,
16-
genSummary,
17-
runTypedoc,
18-
} from '../typedoc';
10+
import { translate } from '../translate';
11+
import { findCategory, getReferenceDoc, runTypedoc } from '../typedoc';
1912

2013
export class ReferenceCommand extends Command {
2114
static paths = [['reference']];
2215

2316
readonly patterns = Option.Array('-p,--pattern', []);
2417
readonly lang = LanguageOption;
2518
readonly withOutput = Option.Boolean('--with-output', true);
19+
readonly translateAiToken = Option.String('--translate-ai-token', {
20+
description:
21+
'Translate documentation with OpenAI when the token is given. Only works when language options is not "en".',
22+
env: 'TRANSLATE_AI_TOKEN',
23+
});
24+
readonly translateAiModel = Option.String('--translate-ai-model', {
25+
description: 'AI model for when translating documentation with OpenAI. Default model is "gpt-4o".',
26+
env: 'TRANSLATE_AI_MODEL',
27+
});
2628

2729
async execute() {
2830
const rootDir = await findRootDir();
@@ -36,7 +38,7 @@ export class ReferenceCommand extends Command {
3638
const references: Array<{
3739
name: string;
3840
category: string[];
39-
doc: string;
41+
doc: ReferenceDoc;
4042
}> = [];
4143
this.traverseReflections(project.children!, reflection => {
4244
const category = findCategory(reflection);
@@ -53,9 +55,13 @@ export class ReferenceCommand extends Command {
5355
references.push({
5456
name: reflection.name,
5557
category,
56-
doc: this.genReferenceDoc(reflection),
58+
doc: getReferenceDoc(reflection),
5759
});
5860
});
61+
62+
const ai =
63+
this.translateAiToken != null && this.lang !== 'en' ? new OpenAI({ apiKey: this.translateAiToken }) : null;
64+
5965
for (let i = 0; i < references.length; i += 1) {
6066
const reference = references[i]!;
6167
const filename =
@@ -64,7 +70,11 @@ export class ReferenceCommand extends Command {
6470
: path.join(this.lang, 'reference', ...reference.category, `${reference.name}.md`);
6571
const filepath = path.join(rootDir, 'docs', filename);
6672
await fs.mkdir(path.dirname(filepath), { recursive: true });
67-
await fs.writeFile(filepath, reference.doc, 'utf8');
73+
const doc =
74+
ai != null
75+
? await translate(ai, reference.doc, this.lang, { model: this.translateAiModel as OpenAI.ChatModel })
76+
: reference.doc;
77+
await fs.writeFile(filepath, renderReferenceDoc(doc, this.lang), 'utf8');
6878
console.log(`[${i + 1}/${references.length}] ${filename} generated`);
6979
}
7080
}
@@ -83,23 +93,4 @@ export class ReferenceCommand extends Command {
8393
}
8494
}
8595
}
86-
87-
private genReferenceDoc(reflection: DeclarationReflection) {
88-
const sig = reflection.signatures?.[0];
89-
if (sig == null) {
90-
throw new Error(`Signature not found: ${reflection.name}`);
91-
}
92-
93-
const summary = genSummary(sig);
94-
const signature = genSignature(sig, { lang: this.lang });
95-
const parameters = genParameters(sig, { lang: this.lang });
96-
const returns = genReturns(sig, { lang: this.lang });
97-
const errors = genErrors(sig, { lang: this.lang });
98-
const example = genExample(sig, { lang: this.lang });
99-
100-
return [`# ${reflection.name}`, summary, signature, parameters, returns, errors, example]
101-
.flat()
102-
.filter(isNotNil)
103-
.join('\n\n');
104-
}
10596
}

docs/gen/doc.ts

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import { isNotNil } from 'es-toolkit';
2+
import { escapeHtml, paramLIHtml, paramULHtml } from './html';
3+
import type { Language } from './lang';
4+
import { t } from './texts';
5+
6+
export interface ParameterDoc {
7+
name: string;
8+
type: string;
9+
required?: boolean;
10+
description?: string;
11+
children?: string | ParameterDoc[];
12+
}
13+
14+
export interface ReturnsDoc {
15+
type: string;
16+
description?: string;
17+
children?: string | ParameterDoc[];
18+
}
19+
20+
export interface ReferenceDoc {
21+
name: string;
22+
summary?: string;
23+
signature?: string;
24+
parameters?: ParameterDoc[];
25+
returns?: ReturnsDoc;
26+
errors?: string[];
27+
examples?: string;
28+
}
29+
30+
function renderParameterDoc(doc: ParameterDoc, lang: Language, root = true): string {
31+
const { name, type, required, description, children } = doc;
32+
return paramLIHtml({
33+
name,
34+
type,
35+
required,
36+
description,
37+
children:
38+
typeof children === 'string'
39+
? `<p class="param-description">${escapeHtml(children)}</p>`
40+
: Array.isArray(children) && children.length > 0
41+
? paramULHtml(children.map(x => renderParameterDoc(x, lang, false)))
42+
: undefined,
43+
root,
44+
lang,
45+
});
46+
}
47+
48+
export function renderReferenceDoc(doc: ReferenceDoc, lang: Language): string {
49+
const { name, summary, signature, parameters, returns, errors, examples } = doc;
50+
51+
const signatureDoc = signature != null ? [`## ${t('signature', lang)}`, '', signature].join('\n') : null;
52+
53+
const parametersDocs =
54+
parameters != null && parameters.length > 0
55+
? [
56+
`### ${t('parameters', lang)}`,
57+
'',
58+
paramULHtml(parameters.map(parameter => renderParameterDoc(parameter, lang))),
59+
].join('\n')
60+
: null;
61+
62+
const returnsDoc =
63+
returns != null
64+
? [
65+
`### ${t('returns', lang)}`,
66+
'',
67+
paramULHtml(
68+
paramLIHtml({
69+
required: false,
70+
type: returns.type,
71+
description: returns.description,
72+
children:
73+
typeof returns.children === 'string'
74+
? `<p class="param-description">${escapeHtml(returns.children)}</p>`
75+
: Array.isArray(returns.children) && returns.children.length > 0
76+
? paramULHtml(returns.children.map(x => renderParameterDoc(x, lang, false)))
77+
: undefined,
78+
root: true,
79+
lang,
80+
})
81+
),
82+
].join('\n')
83+
: null;
84+
const errorsDoc =
85+
errors != null && errors.length > 0
86+
? [
87+
`### ${t('errors', lang)}`,
88+
'',
89+
paramULHtml(
90+
errors.map(error =>
91+
paramLIHtml({
92+
required: false,
93+
type: 'Error',
94+
description: error,
95+
root: true,
96+
lang,
97+
})
98+
)
99+
),
100+
].join('\n')
101+
: null;
102+
const examplesDoc = examples != null ? [`## ${t('examples', lang)}`, '', examples].join('\n') : null;
103+
return [`# ${name}`, summary, signatureDoc, parametersDocs, returnsDoc, errorsDoc, examplesDoc]
104+
.filter(isNotNil)
105+
.join('\n\n');
106+
}

docs/gen/lang.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ const LANGUAGES = ['en', 'ko'] as const;
99

1010
export type Language = (typeof LANGUAGES)[number];
1111

12-
export const LanguageOption = Option.String('--lang', 'en', {
12+
export const LanguageOption = Option.String('-l,--lang', 'en', {
1313
description: 'Language to generate documentation',
1414
validator: isEnum(LANGUAGES),
1515
});

docs/gen/texts.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { Language } from './lang';
22

33
const texts = {
4-
singature: {
4+
signature: {
55
en: 'Signature',
66
ko: '시그니처',
77
},
@@ -13,7 +13,7 @@ const texts = {
1313
en: 'Returns',
1414
ko: '반환 값',
1515
},
16-
example: {
16+
examples: {
1717
en: 'Examples',
1818
ko: '예제',
1919
},

docs/gen/translate.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import type OpenAI from 'openai';
2+
import type { ReferenceDoc } from './doc';
3+
import type { Language } from './lang';
4+
5+
interface Options {
6+
/** @defaultValue 'gpt-4o' */
7+
model?: OpenAI.ChatModel;
8+
}
9+
10+
export async function translate(
11+
ai: OpenAI,
12+
doc: ReferenceDoc,
13+
lang: Language,
14+
options?: Options
15+
): Promise<ReferenceDoc> {
16+
const prompt = `
17+
Always answer in the JSON format as given in the input, without triple backticks.
18+
Translate the following JSON to ${lang}.
19+
20+
If translating in Korean, write the sentence in 해요 style.
21+
If translating in Korean, translate "Git Object" as "Git 개체".
22+
If translating in Korean, translate "Tree" as "트리".
23+
If translating in Korean, translate "Repository" as "리포지토리".
24+
If translating in Japanese, finish the sentence in ます style.
25+
Finish with a noun if it is a explanation for a parameter or a return value.
26+
27+
===
28+
\`\`\`
29+
${JSON.stringify(doc, null, 2)}
30+
\`\`\`
31+
`;
32+
33+
const response = await ai.chat.completions.create({
34+
model: options?.model ?? 'gpt-4o',
35+
messages: [{ role: 'user', content: prompt }],
36+
});
37+
38+
const translatedItem = response.choices[0]?.message.content;
39+
if (translatedItem == null) {
40+
throw new Error(`API Error while translating ${doc.name} to ${lang}.`);
41+
}
42+
const translated = JSON.parse(translatedItem) as ReferenceDoc;
43+
return { ...translated, name: doc.name };
44+
}

0 commit comments

Comments
 (0)