Skip to content

fix: translate recommendations.ts #1029

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Jun 4, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .prettierignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
*.md

origin

adev-ja
608 changes: 304 additions & 304 deletions adev-ja/src/app/features/update/recommendations.ts

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,10 @@
"packageManager": "yarn@1.22.10",
"devDependencies": {
"@google/genai": "^0.8.0",
"@types/cli-progress": "^3.11.6",
"@types/node": "20.14.10",
"chokidar": "3.6.0",
"cli-progress": "^3.12.0",
"consola": "3.2.3",
"execa": "^9.3.0",
"globby": "14.0.2",
Expand Down
2 changes: 1 addition & 1 deletion tools/translator/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ async function main() {
const model = process.env.GEMINI_MODEL || 'gemini-2.0-flash';

const translator = new GeminiTranslator(apiKey, model);
const translated = await translator.translate(content, prh);
const translated = await translator.translate(file, content, prh);

console.log(translated);
await writeTranslatedContent(file, translated, write);
Expand Down
14 changes: 0 additions & 14 deletions tools/translator/markdown.ts

This file was deleted.

190 changes: 142 additions & 48 deletions tools/translator/translate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,16 @@
*/

import { GoogleGenAI } from '@google/genai';
import { SingleBar } from 'cli-progress';
import { consola } from 'consola';
import { setTimeout } from 'node:timers/promises';
import { renderMarkdown, splitMarkdown } from './markdown';
import {
ContentType,
getContentType,
renderContent,
splitMarkdown,
splitRecommendations,
} from './utils';

export class GeminiTranslator {
readonly #client: GoogleGenAI;
Expand All @@ -20,20 +28,82 @@ export class GeminiTranslator {
console.log(`Using model: ${model}`);
}

async translate(content: string, prh: string): Promise<string> {
const systemInstruction = `
async translate(
filename: string,
content: string,
prh: string
): Promise<string> {
const contentType = getContentType(filename);
const systemInstruction = getSystemInstruction(contentType, prh);

const chat = this.#client.chats.create({
model: this.#model,
config: { systemInstruction, temperature: 0.1 },
});

consola.start(`Starting translation for ${filename}`);
await chat
.sendMessage({
message: [
`これから ${filename} の翻訳作業を開始します。次のメッセージからテキスト断片を入力するので、日本語に翻訳して出力してください。今回の翻訳タスクと遵守するルールをおさらいしてください。`,
],
})
.then((response) => {
if (response.text) {
consola.info(`Gemini: ${response.text}`);
}
});

const progress = new SingleBar({});

const blocks =
contentType === 'markdown'
? splitMarkdown(content)
: splitRecommendations(content);

progress.start(blocks.length, 0);
const rpm = 10; // Requests per minute
const waitTime = Math.floor((60 * 1000) / rpm);

const translated = [];
for (const block of blocks) {
const prompt = block.trim();
const delay = setTimeout(waitTime);
const response = await chat.sendMessage({ message: [prompt] });
translated.push(response.text ?? ''); // Fallback in case of no response

progress.increment();
await delay; // Avoid rate limiting
}

progress.stop();
return renderContent(contentType, translated);
}
}

function getSystemInstruction(contentType: ContentType, prh: string): string {
return `
あなたはオープンソースライブラリの開発者向けドキュメントの翻訳者です。
入力として与えられたテキストに含まれる英語を日本語に翻訳します。

## Task

ユーザーはテキスト全体を分割し、断片ごとに翻訳を依頼します。
あなたは与えられた断片を日本語に翻訳し、翻訳結果だけを出力します。
前回までの翻訳結果を参照しながら、テキスト全体での表現の一貫性を保つようにしてください。

${(contentType === 'markdown'
? `
## Rules
翻訳は次のルールに従います。

- 見出しレベル("#")の数を必ず維持する。
- 例: "# Security" → "# セキュリティ"
- Markdownの構造の変更は禁止されています。
- 見出しレベル("#")の数を必ず維持する。
- 例: "# Security" → "# セキュリティ"
- 改行やインデントの数を必ず維持する。
- トップレベル("<h1>")以外の見出しに限り、元の見出しをlower caseでハイフン結合したアンカーIDとして使用する
- 例: "# Security" → "# セキュリティ"
- 例: "## How to use Angular" → "## Angularの使い方 {#how-to-use-angular}"
- 改行やインデントの数を必ず維持する。
- 英単語の前後にスペースを入れない。
- bad: "Angular の使い方"
- good: "Angularの使い方"
Expand All @@ -43,18 +113,6 @@ export class GeminiTranslator {
- 冗長な表現を避け、自然な日本語にする。
- 例: 「することができます」→「できます」

表記揺れや不自然な日本語を避けるため、YAML形式で定義されているPRH(proofreading helper)ルールを使用して、翻訳後のテキストを校正します。
次のPRHルールを使用してください。
---
${prh}
---

## Task

ユーザーはテキスト全体を分割し、断片ごとに翻訳を依頼します。
あなたは与えられた断片を日本語に翻訳し、Markdown形式で出力します。
前回の翻訳結果を参照しながら、テキスト全体での表現の一貫性を保つようにしてください。

入力例:

---
Expand All @@ -72,41 +130,77 @@ It doesn't cover application-level security, such as authentication and authoriz
このトピックでは、クロスサイトスクリプティング攻撃などの一般的なWebアプリケーションの脆弱性や攻撃に対する、Angularの組み込みの保護について説明します。
認証や認可など、アプリケーションレベルのセキュリティは扱いません。
---
`
: contentType === 'recommendations'
? `
recommendations.tsは次のような形式のオブジェクトを含むTypeScriptファイルです。

---
export const RECOMMENDATIONS: Step[] = [
{
possibleIn: 200,
necessaryAsOf: 400,
level: ApplicationComplexity.Basic,
step: 'Extends OnInit',
action:
"Ensure you don't use \`extends OnInit\`, or use \`extends\` with any lifecycle event. Instead use \`implements <lifecycle event>.\`",
},
{
possibleIn: 200,
necessaryAsOf: 400,
level: ApplicationComplexity.Advanced,
step: 'Deep Imports',
action:
'Stop using deep imports, these symbols are now marked with ɵ and are not part of our public API.',
},
---

`.trim();

const chat = this.#client.chats.create({
model: this.#model,
config: {
systemInstruction,
temperature: 0.1,
},
});
## Rules
翻訳は次のルールに従います。
- **翻訳対象となるのは "action" フィールドの文字列リテラルのみです。**
- 原文に存在しない行を追加することは禁止されています。
- 原文に存在する行を削除することは禁止されています。
- ソースコードの構造の変更は禁止されています。
- コードのロジックや構造の変更は禁止されています。
- ソースコードのキーワードや構文の翻訳は禁止されています。
- 変数名や関数名の翻訳は禁止されています。

await chat.sendMessage({
message: [
`これから翻訳作業を開始します。テキスト断片を入力するので、日本語に翻訳して出力してください。`,
],
});
入力例:
---
export const RECOMMENDATIONS: Step[] = [
{
possibleIn: 200,
necessaryAsOf: 400,
level: ApplicationComplexity.Basic,
step: 'Extends OnInit',
action:
"Ensure you don't use \`extends OnInit\`, or use \`extends\` with any lifecycle event. Instead use \`implements <lifecycle event>.\`",
},
---

const blocks = splitMarkdown(content);
const translated = [];
出力例:
---
export const RECOMMENDATIONS: Step[] = [
{
possibleIn: 200,
necessaryAsOf: 400,
level: ApplicationComplexity.Basic,
step: 'Extends OnInit',
action:
'\`OnInit\`を継承しない、あるいはライフサイクルイベントを使用する場合は\`implements <lifecycle event>\`を使用してください。',
},
---

for (const block of blocks) {
const prompt = block.trim();
const response = await chat.sendMessage({
message: [prompt],
});
`
: ``
).trim()};

if (response.text) {
translated.push(response.text);
} else {
translated.push(''); // Fallback in case of no response
}
## 翻訳後の校正

await setTimeout(3000); // Rate limiting
}
return renderMarkdown(translated);
}
表記揺れや不自然な日本語を避けるため、YAML形式で定義されているPRH(proofreading helper)ルールを使用して、翻訳後のテキストを校正します。
次のPRHルールを使用してください。
---
${prh}
---
`.trim();
}
61 changes: 61 additions & 0 deletions tools/translator/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
export type ContentBlock = string;
export type ContentType = 'markdown' | 'recommendations';

export function splitMarkdown(content: string): ContentBlock[] {
// split content by heading lines (lines starting with ##)
return content.split(/\n(?=##\s)/);
}

export function splitRecommendations(content: string): ContentBlock[] {
// split content by 300 lines
const size = 300;
if (content.length <= size) {
return [content];
}
const lines = content.split('\n');
const blocks: ContentBlock[] = [];
for (let i = 0; i < lines.length; i += size) {
blocks.push(lines.slice(i, i + size).join('\n'));
}
return blocks;
}

export async function renderContent(
contentType: ContentType,
blocks: ContentBlock[]
) {
const content = blocks
.map((block) => {
if (contentType === 'markdown') {
// For markdown, ensure each block ends with a newline
return block;
} else {
// For code, ensure no trailing newline
return stripMarkdownBackticks(block);
}
})
.map((block) => (block.endsWith('\n') ? block.replace(/\n$/, '') : block))
.join('\n\n');
// add trailing newline
return content + '\n';
}

export function getContentType(filename: string): ContentType {
// Determine content type based on file extension
if (filename.endsWith('.md')) {
return 'markdown';
} else if (filename.includes('recommendations')) {
return 'recommendations';
}
// Default to markdown for other file types
return 'markdown';
}

export function stripMarkdownBackticks(content: string): string {
// Trim leading and trailing backticks
// and remove any language specifier
// e.g. ```js or ```typescript
return content
.replace(/^\s*```[a-zA-Z0-9-]*\s*/, '') // leading backticks with optional language
.replace(/\s*```[a-zA-Z0-9-]*\s*$/, ''); // trailing backticks with optional language
}
14 changes: 14 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -496,6 +496,13 @@
resolved "https://registry.npmjs.org/@textlint/utils/-/utils-14.0.4.tgz"
integrity sha512-/ThtVZCB/vB2e8+MnKquCFNO2cKXCPEGxFlkdvJ5g9q9ODpVyFcf2ogYoIlvR7cNotvq67zVjENS7dsGDNFEmw==

"@types/cli-progress@^3.11.6":
version "3.11.6"
resolved "https://registry.yarnpkg.com/@types/cli-progress/-/cli-progress-3.11.6.tgz#94b334ebe4190f710e51c1bf9b4fedb681fa9e45"
integrity sha512-cE3+jb9WRlu+uOSAugewNpITJDt1VF8dHOopPO4IABFc3SXYL5WE/+PTz/FCdZRRfIujiWW3n3aMbv1eIGVRWA==
dependencies:
"@types/node" "*"

"@types/mdast@^3.0.0":
version "3.0.10"
resolved "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.10.tgz"
Expand Down Expand Up @@ -760,6 +767,13 @@ chokidar@3.6.0:
optionalDependencies:
fsevents "~2.3.2"

cli-progress@^3.12.0:
version "3.12.0"
resolved "https://registry.yarnpkg.com/cli-progress/-/cli-progress-3.12.0.tgz#807ee14b66bcc086258e444ad0f19e7d42577942"
integrity sha512-tRkV3HJ1ASwm19THiiLIXLO7Im7wlTuKnvkYaTkyoAPefqjNg7W7DHKUlGRxy9vxDvbyCYQkQozvptuMkGCg8A==
dependencies:
string-width "^4.2.3"

clone-regexp@^1.0.0:
version "1.0.1"
resolved "https://registry.npmjs.org/clone-regexp/-/clone-regexp-1.0.1.tgz"
Expand Down
Loading