Skip to content

Commit 0b86a1d

Browse files
authored
feat(ci): add automated untranslated files tracking issue (#1101)
* feat(ci): add untranslated files tracking issue automation - Add --json flag to tools/list-untranslated.ts - Create GitHub Actions workflow for automatic issue updates - Implement issue update script with category grouping - Add pre-filled issue creation links for each file * chore: trigger workflow * fix(ci): pin actions to commit SHAs * fix(ci): suppress pnpm output in JSON parsing * fix(ci): convert script to ESM format * fix(ci): use absolute path for ESM import * fix(ci): use npx tsx directly to avoid pnpm output * chore(ci): update issue title and remove feature branch trigger * chore(ci): re-add feature branch trigger for testing * docs(ci): add JSDoc type annotations to sync script * style(ci): remove bold formatting from file names * fix(ci): update existing issue with new title and format * chore(ci): remove backward compatibility for old issue title * feat(ci): open preview links in new tab * revert: remove target=_blank from preview links * feat(tools): exclude tutorial config.json from untranslated list * feat(ci): add Tools category to untranslated files list * feat(ci): add Ecosystem category to untranslated files list * feat(ci): integrate Translation Checkout issues with tracking list * test: trigger workflow to verify Translation Checkout integration * test: verify Translation Checkout integration with Issue #5 * fix: use prefix matching for Translation Checkout issues to support directory declarations * fix: use simple #{num} format for issue links and remove feature branch trigger * test: re-add feature branch trigger for final verification * chore: remove feature branch trigger
1 parent b526de7 commit 0b86a1d

File tree

3 files changed

+375
-8
lines changed

3 files changed

+375
-8
lines changed
Lines changed: 292 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,292 @@
1+
/**
2+
* @fileoverview GitHub Actions script to sync untranslated files tracking issue
3+
*/
4+
5+
/**
6+
* @typedef {Object} UntranslatedFile
7+
* @property {string} path - File path relative to adev-ja
8+
* @property {string} category - File category (guide, tutorial, etc.)
9+
* @property {string} extension - File extension without dot
10+
*/
11+
12+
/**
13+
* @typedef {Object} FilesData
14+
* @property {number} count - Total number of untranslated files
15+
* @property {UntranslatedFile[]} files - Array of untranslated files
16+
*/
17+
18+
/**
19+
* @typedef {Object} FileLinks
20+
* @property {string} githubUrl - GitHub blob URL
21+
* @property {string|null} previewUrl - Preview URL on angular.jp (null for non-md files)
22+
* @property {string} issueUrl - Issue creation URL with pre-filled title
23+
*/
24+
25+
/**
26+
* @typedef {Object} GitHubContext
27+
* @property {Object} repo
28+
* @property {string} repo.owner - Repository owner
29+
* @property {string} repo.repo - Repository name
30+
*/
31+
32+
/**
33+
* @typedef {Object} GitHubAPI
34+
* @property {Object} rest
35+
* @property {Object} rest.issues
36+
* @property {Function} rest.issues.listForRepo
37+
* @property {Function} rest.issues.create
38+
* @property {Function} rest.issues.update
39+
*/
40+
41+
/**
42+
* @typedef {Object} ActionsCore
43+
* @property {Function} info - Log info message
44+
*/
45+
46+
const ISSUE_TITLE = 'Tracking: 未翻訳ドキュメント一覧';
47+
const LABELS = ['type: translation', '翻訳者募集中'];
48+
49+
/** @type {Record<string, string>} */
50+
const CATEGORY_EMOJIS = {
51+
guide: '📖 Guide',
52+
tutorial: '🎓 Tutorial',
53+
reference: '📚 Reference',
54+
'best-practices': '⚡ Best Practices',
55+
cli: '🔧 CLI',
56+
tools: '🛠️ Tools',
57+
ecosystem: '🌐 Ecosystem',
58+
app: '🧩 Components/App',
59+
other: '📦 その他'
60+
};
61+
62+
/** @type {string[]} */
63+
const CATEGORY_ORDER = ['guide', 'tutorial', 'reference', 'best-practices', 'cli', 'tools', 'ecosystem', 'app', 'other'];
64+
65+
/**
66+
* Generate URLs for a file
67+
* @param {string} filepath - File path relative to adev-ja
68+
* @returns {FileLinks} Object containing GitHub, preview, and issue URLs
69+
*/
70+
function generateLinks(filepath) {
71+
const githubUrl = `https://github.com/angular/angular-ja/blob/main/adev-ja/${filepath}`;
72+
73+
// タイトル生成: パスから拡張子を除去したシンプルな形式
74+
const title = filepath
75+
.replace('src/content/', '')
76+
.replace(/\.(md|ts|html|json)$/, '');
77+
78+
const issueUrl = `https://github.com/angular/angular-ja/issues/new?template=----.md&title=${encodeURIComponent(title + ' の翻訳')}`;
79+
80+
// .mdファイルのみプレビューURL生成
81+
let previewUrl = null;
82+
if (filepath.endsWith('.md')) {
83+
const previewPath = filepath
84+
.replace('src/content/', '')
85+
.replace(/\/README\.md$/, '') // READMEの場合はディレクトリのみ
86+
.replace(/\.md$/, '');
87+
previewUrl = `https://angular.jp/${previewPath}`;
88+
}
89+
90+
return { githubUrl, previewUrl, issueUrl };
91+
}
92+
93+
/**
94+
* Format a file entry for the issue body
95+
* @param {string} filepath - File path relative to adev-ja
96+
* @param {FileLinks} links - Object containing URLs for the file
97+
* @param {number|null} checkoutIssueNumber - Translation Checkout issue number if exists
98+
* @returns {string} Markdown formatted list item
99+
*/
100+
function formatFileEntry(filepath, links, checkoutIssueNumber = null) {
101+
const displayName = filepath.replace('src/content/', '');
102+
103+
let linksText = `[GitHub](${links.githubUrl})`;
104+
if (links.previewUrl) {
105+
linksText += ` | [プレビュー](${links.previewUrl})`;
106+
}
107+
108+
if (checkoutIssueNumber) {
109+
linksText += ` | #${checkoutIssueNumber}`;
110+
return `- [x] ${displayName} (${linksText})`;
111+
} else {
112+
linksText += ` | [📝 翻訳宣言](${links.issueUrl})`;
113+
return `- [ ] ${displayName} (${linksText})`;
114+
}
115+
}
116+
117+
/**
118+
* Group files by category
119+
* @param {UntranslatedFile[]} files - Array of untranslated files
120+
* @returns {Record<string, UntranslatedFile[]>} Files grouped by category
121+
*/
122+
function groupByCategory(files) {
123+
const groups = {};
124+
for (const file of files) {
125+
const category = file.category;
126+
if (!groups[category]) {
127+
groups[category] = [];
128+
}
129+
groups[category].push(file);
130+
}
131+
return groups;
132+
}
133+
134+
/**
135+
* Generate issue body
136+
* @param {FilesData} filesData - Object containing untranslated files data
137+
* @param {Map<string, number>} checkoutIssuesMap - Map of file paths to issue numbers
138+
* @returns {string} Markdown formatted issue body
139+
*/
140+
function generateIssueBody(filesData, checkoutIssuesMap) {
141+
const { count, files } = filesData;
142+
143+
if (count === 0) {
144+
return `## 🎉 全てのファイルが翻訳されました!
145+
146+
**最終更新**: ${new Date().toISOString()}
147+
148+
現在、未翻訳のファイルはありません。素晴らしい貢献をありがとうございます!
149+
150+
---
151+
152+
## 📝 翻訳ガイド
153+
154+
今後新しい未翻訳ファイルが追加された場合、このIssueが自動的に更新されます。
155+
156+
- [翻訳ガイドライン](https://github.com/angular/angular-ja/blob/main/CONTRIBUTING.md)
157+
`;
158+
}
159+
160+
const groups = groupByCategory(files);
161+
162+
let body = `## 📋 未翻訳ドキュメント一覧
163+
164+
このIssueは自動的に更新されます。翻訳したいファイルの「📝 翻訳宣言」リンクから翻訳宣言Issueを作成してください。
165+
166+
**最終更新**: ${new Date().toISOString()}
167+
**未翻訳ファイル数**: ${count}
168+
169+
---
170+
171+
`;
172+
173+
// カテゴリ順にセクションを生成
174+
for (const category of CATEGORY_ORDER) {
175+
if (!groups[category] || groups[category].length === 0) continue;
176+
177+
const categoryFiles = groups[category];
178+
const emoji = CATEGORY_EMOJIS[category] || category;
179+
180+
body += `### ${emoji} (${categoryFiles.length}件)\n\n`;
181+
182+
for (const file of categoryFiles) {
183+
const links = generateLinks(file.path);
184+
const checkoutIssueNumber = checkoutIssuesMap.get(file.path) || null;
185+
body += formatFileEntry(file.path, links, checkoutIssueNumber) + '\n';
186+
}
187+
188+
body += '\n';
189+
}
190+
191+
body += `---
192+
193+
## 📝 翻訳の始め方
194+
195+
1. 上記リストから翻訳したいファイルを選ぶ
196+
2. 「📝 翻訳宣言」リンクをクリックしてIssueを作成
197+
3. [翻訳ガイド](https://github.com/angular/angular-ja/blob/main/CONTRIBUTING.md)に従って作業開始
198+
`;
199+
200+
return body;
201+
}
202+
203+
/**
204+
* Main function
205+
* @param {Object} params - Parameters
206+
* @param {GitHubAPI} params.github - GitHub API instance
207+
* @param {GitHubContext} params.context - GitHub Actions context
208+
* @param {ActionsCore} params.core - GitHub Actions core utilities
209+
* @param {FilesData} params.filesData - Untranslated files data
210+
* @returns {Promise<void>}
211+
*/
212+
export default async ({github, context, core, filesData}) => {
213+
const owner = context.repo.owner;
214+
const repo = context.repo.repo;
215+
216+
core.info(`Processing ${filesData.count} untranslated files...`);
217+
218+
// Translation Checkout ラベルの全Issue (open only) を取得
219+
const { data: checkoutIssues } = await github.rest.issues.listForRepo({
220+
owner,
221+
repo,
222+
state: 'open',
223+
labels: 'type: Translation Checkout'
224+
});
225+
226+
core.info(`Found ${checkoutIssues.length} Translation Checkout issues`);
227+
228+
// Issueタイトルからファイルパスを抽出してマップを作成
229+
// タイトル形式: "{ファイルパス} の翻訳"
230+
// 前方一致でマッチング(ディレクトリ名での宣言に対応)
231+
const checkoutIssuesMap = new Map();
232+
for (const issue of checkoutIssues) {
233+
const match = issue.title.match(/^(.+)\s+$/);
234+
if (match) {
235+
const declaredPath = `src/content/${match[1]}`;
236+
// 各未翻訳ファイルに対して前方一致チェック
237+
for (const file of filesData.files) {
238+
if (file.path.startsWith(declaredPath)) {
239+
checkoutIssuesMap.set(file.path, issue.number);
240+
}
241+
}
242+
}
243+
}
244+
245+
core.info(`Mapped ${checkoutIssuesMap.size} files to checkout issues`);
246+
247+
// 既存のトラッキングIssueを検索 (state: all で closed も含む)
248+
const { data: issues } = await github.rest.issues.listForRepo({
249+
owner,
250+
repo,
251+
state: 'all',
252+
labels: LABELS[0],
253+
creator: 'github-actions[bot]'
254+
});
255+
256+
const trackingIssue = issues.find(issue => issue.title === ISSUE_TITLE);
257+
258+
const issueBody = generateIssueBody(filesData, checkoutIssuesMap);
259+
260+
if (trackingIssue) {
261+
core.info(`Found existing tracking issue #${trackingIssue.number}`);
262+
263+
// Issueを更新 (タイトルも更新して新しい形式に移行)
264+
await github.rest.issues.update({
265+
owner,
266+
repo,
267+
issue_number: trackingIssue.number,
268+
title: ISSUE_TITLE,
269+
body: issueBody,
270+
state: 'open' // closed状態の場合はreopen
271+
});
272+
273+
core.info(`Updated tracking issue #${trackingIssue.number}`);
274+
275+
if (trackingIssue.state === 'closed') {
276+
core.info(`Reopened tracking issue #${trackingIssue.number}`);
277+
}
278+
} else {
279+
// 新規Issueを作成
280+
const { data: newIssue } = await github.rest.issues.create({
281+
owner,
282+
repo,
283+
title: ISSUE_TITLE,
284+
body: issueBody,
285+
labels: LABELS
286+
});
287+
288+
core.info(`Created new tracking issue #${newIssue.number}`);
289+
}
290+
291+
core.info('Done!');
292+
};
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
name: Sync Untranslated Files Issue
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
issues:
8+
types: [opened, closed, reopened, labeled]
9+
workflow_dispatch:
10+
11+
permissions:
12+
contents: read
13+
issues: write
14+
15+
jobs:
16+
update-issue:
17+
runs-on: ubuntu-latest
18+
steps:
19+
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
20+
with:
21+
submodules: true
22+
23+
- name: Setup pnpm
24+
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # 4.1.0
25+
26+
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
27+
with:
28+
node-version-file: '.node-version'
29+
cache: pnpm
30+
31+
- run: pnpm install
32+
33+
- name: Get untranslated files
34+
id: files
35+
run: |
36+
FILES_JSON=$(npx tsx tools/list-untranslated.ts --json)
37+
echo "data<<EOF" >> $GITHUB_OUTPUT
38+
echo "$FILES_JSON" >> $GITHUB_OUTPUT
39+
echo "EOF" >> $GITHUB_OUTPUT
40+
41+
- name: Update tracking issue
42+
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
43+
with:
44+
script: |
45+
const { default: syncIssue } = await import('${{ github.workspace }}/.github/scripts/sync-untranslated-issue.mjs');
46+
const filesData = JSON.parse(`${{ steps.files.outputs.data }}`);
47+
await syncIssue({github, context, core, filesData});

0 commit comments

Comments
 (0)