Skip to content

Commit 0c6bbd0

Browse files
authored
fix: translate recommendations.ts (#1029)
recommendations.tsの自動翻訳プロンプトも改善(まだ十分ではない)
1 parent b1e485b commit 0c6bbd0

File tree

8 files changed

+525
-368
lines changed

8 files changed

+525
-368
lines changed

.prettierignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
*.md
22

33
origin
4-
4+
adev-ja

adev-ja/src/app/features/update/recommendations.ts

Lines changed: 304 additions & 304 deletions
Large diffs are not rendered by default.

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,10 @@
1818
"packageManager": "[email protected]",
1919
"devDependencies": {
2020
"@google/genai": "^0.8.0",
21+
"@types/cli-progress": "^3.11.6",
2122
"@types/node": "20.14.10",
2223
"chokidar": "3.6.0",
24+
"cli-progress": "^3.12.0",
2325
"consola": "3.2.3",
2426
"execa": "^9.3.0",
2527
"globby": "14.0.2",

tools/translator/main.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ async function main() {
3333
const model = process.env.GEMINI_MODEL || 'gemini-2.0-flash';
3434

3535
const translator = new GeminiTranslator(apiKey, model);
36-
const translated = await translator.translate(content, prh);
36+
const translated = await translator.translate(file, content, prh);
3737

3838
console.log(translated);
3939
await writeTranslatedContent(file, translated, write);

tools/translator/markdown.ts

Lines changed: 0 additions & 14 deletions
This file was deleted.

tools/translator/translate.ts

Lines changed: 142 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,16 @@
77
*/
88

99
import { GoogleGenAI } from '@google/genai';
10+
import { SingleBar } from 'cli-progress';
11+
import { consola } from 'consola';
1012
import { setTimeout } from 'node:timers/promises';
11-
import { renderMarkdown, splitMarkdown } from './markdown';
13+
import {
14+
ContentType,
15+
getContentType,
16+
renderContent,
17+
splitMarkdown,
18+
splitRecommendations,
19+
} from './utils';
1220

1321
export class GeminiTranslator {
1422
readonly #client: GoogleGenAI;
@@ -20,20 +28,82 @@ export class GeminiTranslator {
2028
console.log(`Using model: ${model}`);
2129
}
2230

23-
async translate(content: string, prh: string): Promise<string> {
24-
const systemInstruction = `
31+
async translate(
32+
filename: string,
33+
content: string,
34+
prh: string
35+
): Promise<string> {
36+
const contentType = getContentType(filename);
37+
const systemInstruction = getSystemInstruction(contentType, prh);
38+
39+
const chat = this.#client.chats.create({
40+
model: this.#model,
41+
config: { systemInstruction, temperature: 0.1 },
42+
});
43+
44+
consola.start(`Starting translation for ${filename}`);
45+
await chat
46+
.sendMessage({
47+
message: [
48+
`これから ${filename} の翻訳作業を開始します。次のメッセージからテキスト断片を入力するので、日本語に翻訳して出力してください。今回の翻訳タスクと遵守するルールをおさらいしてください。`,
49+
],
50+
})
51+
.then((response) => {
52+
if (response.text) {
53+
consola.info(`Gemini: ${response.text}`);
54+
}
55+
});
56+
57+
const progress = new SingleBar({});
58+
59+
const blocks =
60+
contentType === 'markdown'
61+
? splitMarkdown(content)
62+
: splitRecommendations(content);
63+
64+
progress.start(blocks.length, 0);
65+
const rpm = 10; // Requests per minute
66+
const waitTime = Math.floor((60 * 1000) / rpm);
67+
68+
const translated = [];
69+
for (const block of blocks) {
70+
const prompt = block.trim();
71+
const delay = setTimeout(waitTime);
72+
const response = await chat.sendMessage({ message: [prompt] });
73+
translated.push(response.text ?? ''); // Fallback in case of no response
74+
75+
progress.increment();
76+
await delay; // Avoid rate limiting
77+
}
78+
79+
progress.stop();
80+
return renderContent(contentType, translated);
81+
}
82+
}
83+
84+
function getSystemInstruction(contentType: ContentType, prh: string): string {
85+
return `
2586
あなたはオープンソースライブラリの開発者向けドキュメントの翻訳者です。
2687
入力として与えられたテキストに含まれる英語を日本語に翻訳します。
2788
89+
## Task
90+
91+
ユーザーはテキスト全体を分割し、断片ごとに翻訳を依頼します。
92+
あなたは与えられた断片を日本語に翻訳し、翻訳結果だけを出力します。
93+
前回までの翻訳結果を参照しながら、テキスト全体での表現の一貫性を保つようにしてください。
94+
95+
${(contentType === 'markdown'
96+
? `
2897
## Rules
2998
翻訳は次のルールに従います。
3099
31-
- 見出しレベル("#")の数を必ず維持する。
32-
- 例: "# Security" → "# セキュリティ"
100+
- Markdownの構造の変更は禁止されています。
101+
- 見出しレベル("#")の数を必ず維持する。
102+
- 例: "# Security" → "# セキュリティ"
103+
- 改行やインデントの数を必ず維持する。
33104
- トップレベル("<h1>")以外の見出しに限り、元の見出しをlower caseでハイフン結合したアンカーIDとして使用する
34105
- 例: "# Security" → "# セキュリティ"
35106
- 例: "## How to use Angular" → "## Angularの使い方 {#how-to-use-angular}"
36-
- 改行やインデントの数を必ず維持する。
37107
- 英単語の前後にスペースを入れない。
38108
- bad: "Angular の使い方"
39109
- good: "Angularの使い方"
@@ -43,18 +113,6 @@ export class GeminiTranslator {
43113
- 冗長な表現を避け、自然な日本語にする。
44114
- 例: 「することができます」→「できます」
45115
46-
表記揺れや不自然な日本語を避けるため、YAML形式で定義されているPRH(proofreading helper)ルールを使用して、翻訳後のテキストを校正します。
47-
次のPRHルールを使用してください。
48-
---
49-
${prh}
50-
---
51-
52-
## Task
53-
54-
ユーザーはテキスト全体を分割し、断片ごとに翻訳を依頼します。
55-
あなたは与えられた断片を日本語に翻訳し、Markdown形式で出力します。
56-
前回の翻訳結果を参照しながら、テキスト全体での表現の一貫性を保つようにしてください。
57-
58116
入力例:
59117
60118
---
@@ -72,41 +130,77 @@ It doesn't cover application-level security, such as authentication and authoriz
72130
このトピックでは、クロスサイトスクリプティング攻撃などの一般的なWebアプリケーションの脆弱性や攻撃に対する、Angularの組み込みの保護について説明します。
73131
認証や認可など、アプリケーションレベルのセキュリティは扱いません。
74132
---
133+
`
134+
: contentType === 'recommendations'
135+
? `
136+
recommendations.tsは次のような形式のオブジェクトを含むTypeScriptファイルです。
75137
138+
---
139+
export const RECOMMENDATIONS: Step[] = [
140+
{
141+
possibleIn: 200,
142+
necessaryAsOf: 400,
143+
level: ApplicationComplexity.Basic,
144+
step: 'Extends OnInit',
145+
action:
146+
"Ensure you don't use \`extends OnInit\`, or use \`extends\` with any lifecycle event. Instead use \`implements <lifecycle event>.\`",
147+
},
148+
{
149+
possibleIn: 200,
150+
necessaryAsOf: 400,
151+
level: ApplicationComplexity.Advanced,
152+
step: 'Deep Imports',
153+
action:
154+
'Stop using deep imports, these symbols are now marked with ɵ and are not part of our public API.',
155+
},
156+
---
76157
77-
`.trim();
78-
79-
const chat = this.#client.chats.create({
80-
model: this.#model,
81-
config: {
82-
systemInstruction,
83-
temperature: 0.1,
84-
},
85-
});
158+
## Rules
159+
翻訳は次のルールに従います。
160+
- **翻訳対象となるのは "action" フィールドの文字列リテラルのみです。**
161+
- 原文に存在しない行を追加することは禁止されています。
162+
- 原文に存在する行を削除することは禁止されています。
163+
- ソースコードの構造の変更は禁止されています。
164+
- コードのロジックや構造の変更は禁止されています。
165+
- ソースコードのキーワードや構文の翻訳は禁止されています。
166+
- 変数名や関数名の翻訳は禁止されています。
86167
87-
await chat.sendMessage({
88-
message: [
89-
`これから翻訳作業を開始します。テキスト断片を入力するので、日本語に翻訳して出力してください。`,
90-
],
91-
});
168+
入力例:
169+
---
170+
export const RECOMMENDATIONS: Step[] = [
171+
{
172+
possibleIn: 200,
173+
necessaryAsOf: 400,
174+
level: ApplicationComplexity.Basic,
175+
step: 'Extends OnInit',
176+
action:
177+
"Ensure you don't use \`extends OnInit\`, or use \`extends\` with any lifecycle event. Instead use \`implements <lifecycle event>.\`",
178+
},
179+
---
92180
93-
const blocks = splitMarkdown(content);
94-
const translated = [];
181+
出力例:
182+
---
183+
export const RECOMMENDATIONS: Step[] = [
184+
{
185+
possibleIn: 200,
186+
necessaryAsOf: 400,
187+
level: ApplicationComplexity.Basic,
188+
step: 'Extends OnInit',
189+
action:
190+
'\`OnInit\`を継承しない、あるいはライフサイクルイベントを使用する場合は\`implements <lifecycle event>\`を使用してください。',
191+
},
192+
---
95193
96-
for (const block of blocks) {
97-
const prompt = block.trim();
98-
const response = await chat.sendMessage({
99-
message: [prompt],
100-
});
194+
`
195+
: ``
196+
).trim()};
101197
102-
if (response.text) {
103-
translated.push(response.text);
104-
} else {
105-
translated.push(''); // Fallback in case of no response
106-
}
198+
## 翻訳後の校正
107199
108-
await setTimeout(3000); // Rate limiting
109-
}
110-
return renderMarkdown(translated);
111-
}
200+
表記揺れや不自然な日本語を避けるため、YAML形式で定義されているPRH(proofreading helper)ルールを使用して、翻訳後のテキストを校正します。
201+
次のPRHルールを使用してください。
202+
---
203+
${prh}
204+
---
205+
`.trim();
112206
}

tools/translator/utils.ts

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
export type ContentBlock = string;
2+
export type ContentType = 'markdown' | 'recommendations';
3+
4+
export function splitMarkdown(content: string): ContentBlock[] {
5+
// split content by heading lines (lines starting with ##)
6+
return content.split(/\n(?=##\s)/);
7+
}
8+
9+
export function splitRecommendations(content: string): ContentBlock[] {
10+
// split content by 300 lines
11+
const size = 300;
12+
if (content.length <= size) {
13+
return [content];
14+
}
15+
const lines = content.split('\n');
16+
const blocks: ContentBlock[] = [];
17+
for (let i = 0; i < lines.length; i += size) {
18+
blocks.push(lines.slice(i, i + size).join('\n'));
19+
}
20+
return blocks;
21+
}
22+
23+
export async function renderContent(
24+
contentType: ContentType,
25+
blocks: ContentBlock[]
26+
) {
27+
const content = blocks
28+
.map((block) => {
29+
if (contentType === 'markdown') {
30+
// For markdown, ensure each block ends with a newline
31+
return block;
32+
} else {
33+
// For code, ensure no trailing newline
34+
return stripMarkdownBackticks(block);
35+
}
36+
})
37+
.map((block) => (block.endsWith('\n') ? block.replace(/\n$/, '') : block))
38+
.join('\n\n');
39+
// add trailing newline
40+
return content + '\n';
41+
}
42+
43+
export function getContentType(filename: string): ContentType {
44+
// Determine content type based on file extension
45+
if (filename.endsWith('.md')) {
46+
return 'markdown';
47+
} else if (filename.includes('recommendations')) {
48+
return 'recommendations';
49+
}
50+
// Default to markdown for other file types
51+
return 'markdown';
52+
}
53+
54+
export function stripMarkdownBackticks(content: string): string {
55+
// Trim leading and trailing backticks
56+
// and remove any language specifier
57+
// e.g. ```js or ```typescript
58+
return content
59+
.replace(/^\s*```[a-zA-Z0-9-]*\s*/, '') // leading backticks with optional language
60+
.replace(/\s*```[a-zA-Z0-9-]*\s*$/, ''); // trailing backticks with optional language
61+
}

yarn.lock

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -496,6 +496,13 @@
496496
resolved "https://registry.npmjs.org/@textlint/utils/-/utils-14.0.4.tgz"
497497
integrity sha512-/ThtVZCB/vB2e8+MnKquCFNO2cKXCPEGxFlkdvJ5g9q9ODpVyFcf2ogYoIlvR7cNotvq67zVjENS7dsGDNFEmw==
498498

499+
"@types/cli-progress@^3.11.6":
500+
version "3.11.6"
501+
resolved "https://registry.yarnpkg.com/@types/cli-progress/-/cli-progress-3.11.6.tgz#94b334ebe4190f710e51c1bf9b4fedb681fa9e45"
502+
integrity sha512-cE3+jb9WRlu+uOSAugewNpITJDt1VF8dHOopPO4IABFc3SXYL5WE/+PTz/FCdZRRfIujiWW3n3aMbv1eIGVRWA==
503+
dependencies:
504+
"@types/node" "*"
505+
499506
"@types/mdast@^3.0.0":
500507
version "3.0.10"
501508
resolved "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.10.tgz"
@@ -760,6 +767,13 @@ [email protected]:
760767
optionalDependencies:
761768
fsevents "~2.3.2"
762769

770+
cli-progress@^3.12.0:
771+
version "3.12.0"
772+
resolved "https://registry.yarnpkg.com/cli-progress/-/cli-progress-3.12.0.tgz#807ee14b66bcc086258e444ad0f19e7d42577942"
773+
integrity sha512-tRkV3HJ1ASwm19THiiLIXLO7Im7wlTuKnvkYaTkyoAPefqjNg7W7DHKUlGRxy9vxDvbyCYQkQozvptuMkGCg8A==
774+
dependencies:
775+
string-width "^4.2.3"
776+
763777
clone-regexp@^1.0.0:
764778
version "1.0.1"
765779
resolved "https://registry.npmjs.org/clone-regexp/-/clone-regexp-1.0.1.tgz"

0 commit comments

Comments
 (0)