Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
26dfc10
feat(ci): add untranslated files tracking issue automation
lacolaco Nov 21, 2025
c160ea1
chore: trigger workflow
lacolaco Nov 21, 2025
28cec20
fix(ci): pin actions to commit SHAs
lacolaco Nov 21, 2025
25ccc9d
fix(ci): suppress pnpm output in JSON parsing
lacolaco Nov 21, 2025
ef65e27
fix(ci): convert script to ESM format
lacolaco Nov 21, 2025
798765a
fix(ci): use absolute path for ESM import
lacolaco Nov 21, 2025
3e5242b
fix(ci): use npx tsx directly to avoid pnpm output
lacolaco Nov 21, 2025
072a4a9
chore(ci): update issue title and remove feature branch trigger
lacolaco Nov 21, 2025
76abf92
chore(ci): re-add feature branch trigger for testing
lacolaco Nov 21, 2025
0078865
docs(ci): add JSDoc type annotations to sync script
lacolaco Nov 21, 2025
b9d1029
style(ci): remove bold formatting from file names
lacolaco Nov 21, 2025
0e12589
fix(ci): update existing issue with new title and format
lacolaco Nov 21, 2025
c454699
chore(ci): remove backward compatibility for old issue title
lacolaco Nov 21, 2025
f93222f
feat(ci): open preview links in new tab
lacolaco Nov 21, 2025
8491683
revert: remove target=_blank from preview links
lacolaco Nov 21, 2025
ce1c97f
feat(tools): exclude tutorial config.json from untranslated list
lacolaco Nov 21, 2025
ea54d2a
feat(ci): add Tools category to untranslated files list
lacolaco Nov 21, 2025
a6ec129
feat(ci): add Ecosystem category to untranslated files list
lacolaco Nov 21, 2025
c09bcb1
feat(ci): integrate Translation Checkout issues with tracking list
lacolaco Nov 21, 2025
4f5467b
test: trigger workflow to verify Translation Checkout integration
lacolaco Nov 21, 2025
39f0b21
test: verify Translation Checkout integration with Issue #5
lacolaco Nov 21, 2025
74f6276
fix: use prefix matching for Translation Checkout issues to support d…
lacolaco Nov 21, 2025
9f01164
fix: use simple #{num} format for issue links and remove feature bran…
lacolaco Nov 21, 2025
923db89
test: re-add feature branch trigger for final verification
lacolaco Nov 21, 2025
1c04811
chore: remove feature branch trigger
lacolaco Nov 21, 2025
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
292 changes: 292 additions & 0 deletions .github/scripts/sync-untranslated-issue.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,292 @@
/**
* @fileoverview GitHub Actions script to sync untranslated files tracking issue
*/

/**
* @typedef {Object} UntranslatedFile
* @property {string} path - File path relative to adev-ja
* @property {string} category - File category (guide, tutorial, etc.)
* @property {string} extension - File extension without dot
*/

/**
* @typedef {Object} FilesData
* @property {number} count - Total number of untranslated files
* @property {UntranslatedFile[]} files - Array of untranslated files
*/

/**
* @typedef {Object} FileLinks
* @property {string} githubUrl - GitHub blob URL
* @property {string|null} previewUrl - Preview URL on angular.jp (null for non-md files)
* @property {string} issueUrl - Issue creation URL with pre-filled title
*/

/**
* @typedef {Object} GitHubContext
* @property {Object} repo
* @property {string} repo.owner - Repository owner
* @property {string} repo.repo - Repository name
*/

/**
* @typedef {Object} GitHubAPI
* @property {Object} rest
* @property {Object} rest.issues
* @property {Function} rest.issues.listForRepo
* @property {Function} rest.issues.create
* @property {Function} rest.issues.update
*/

/**
* @typedef {Object} ActionsCore
* @property {Function} info - Log info message
*/

const ISSUE_TITLE = 'Tracking: 未翻訳ドキュメント一覧';
const LABELS = ['type: translation', '翻訳者募集中'];

/** @type {Record<string, string>} */
const CATEGORY_EMOJIS = {
guide: '📖 Guide',
tutorial: '🎓 Tutorial',
reference: '📚 Reference',
'best-practices': '⚡ Best Practices',
cli: '🔧 CLI',
tools: '🛠️ Tools',
ecosystem: '🌐 Ecosystem',
app: '🧩 Components/App',
other: '📦 その他'
};

/** @type {string[]} */
const CATEGORY_ORDER = ['guide', 'tutorial', 'reference', 'best-practices', 'cli', 'tools', 'ecosystem', 'app', 'other'];

/**
* Generate URLs for a file
* @param {string} filepath - File path relative to adev-ja
* @returns {FileLinks} Object containing GitHub, preview, and issue URLs
*/
function generateLinks(filepath) {
const githubUrl = `https://github.com/angular/angular-ja/blob/main/adev-ja/${filepath}`;

// タイトル生成: パスから拡張子を除去したシンプルな形式
const title = filepath
.replace('src/content/', '')
.replace(/\.(md|ts|html|json)$/, '');

const issueUrl = `https://github.com/angular/angular-ja/issues/new?template=----.md&title=${encodeURIComponent(title + ' の翻訳')}`;

// .mdファイルのみプレビューURL生成
let previewUrl = null;
if (filepath.endsWith('.md')) {
const previewPath = filepath
.replace('src/content/', '')
.replace(/\/README\.md$/, '') // READMEの場合はディレクトリのみ
.replace(/\.md$/, '');
previewUrl = `https://angular.jp/${previewPath}`;
}

return { githubUrl, previewUrl, issueUrl };
}

/**
* Format a file entry for the issue body
* @param {string} filepath - File path relative to adev-ja
* @param {FileLinks} links - Object containing URLs for the file
* @param {number|null} checkoutIssueNumber - Translation Checkout issue number if exists
* @returns {string} Markdown formatted list item
*/
function formatFileEntry(filepath, links, checkoutIssueNumber = null) {
const displayName = filepath.replace('src/content/', '');

let linksText = `[GitHub](${links.githubUrl})`;
if (links.previewUrl) {
linksText += ` | [プレビュー](${links.previewUrl})`;
}

if (checkoutIssueNumber) {
linksText += ` | #${checkoutIssueNumber}`;
return `- [x] ${displayName} (${linksText})`;
} else {
linksText += ` | [📝 翻訳宣言](${links.issueUrl})`;
return `- [ ] ${displayName} (${linksText})`;
}
}

/**
* Group files by category
* @param {UntranslatedFile[]} files - Array of untranslated files
* @returns {Record<string, UntranslatedFile[]>} Files grouped by category
*/
function groupByCategory(files) {
const groups = {};
for (const file of files) {
const category = file.category;
if (!groups[category]) {
groups[category] = [];
}
groups[category].push(file);
}
return groups;
}

/**
* Generate issue body
* @param {FilesData} filesData - Object containing untranslated files data
* @param {Map<string, number>} checkoutIssuesMap - Map of file paths to issue numbers
* @returns {string} Markdown formatted issue body
*/
function generateIssueBody(filesData, checkoutIssuesMap) {
const { count, files } = filesData;

if (count === 0) {
return `## 🎉 全てのファイルが翻訳されました!

**最終更新**: ${new Date().toISOString()}

現在、未翻訳のファイルはありません。素晴らしい貢献をありがとうございます!

---

## 📝 翻訳ガイド

今後新しい未翻訳ファイルが追加された場合、このIssueが自動的に更新されます。

- [翻訳ガイドライン](https://github.com/angular/angular-ja/blob/main/CONTRIBUTING.md)
`;
}

const groups = groupByCategory(files);

let body = `## 📋 未翻訳ドキュメント一覧

このIssueは自動的に更新されます。翻訳したいファイルの「📝 翻訳宣言」リンクから翻訳宣言Issueを作成してください。

**最終更新**: ${new Date().toISOString()}
**未翻訳ファイル数**: ${count}件

---

`;

// カテゴリ順にセクションを生成
for (const category of CATEGORY_ORDER) {
if (!groups[category] || groups[category].length === 0) continue;

const categoryFiles = groups[category];
const emoji = CATEGORY_EMOJIS[category] || category;

body += `### ${emoji} (${categoryFiles.length}件)\n\n`;

for (const file of categoryFiles) {
const links = generateLinks(file.path);
const checkoutIssueNumber = checkoutIssuesMap.get(file.path) || null;
body += formatFileEntry(file.path, links, checkoutIssueNumber) + '\n';
}

body += '\n';
}

body += `---

## 📝 翻訳の始め方

1. 上記リストから翻訳したいファイルを選ぶ
2. 「📝 翻訳宣言」リンクをクリックしてIssueを作成
3. [翻訳ガイド](https://github.com/angular/angular-ja/blob/main/CONTRIBUTING.md)に従って作業開始
`;

return body;
}

/**
* Main function
* @param {Object} params - Parameters
* @param {GitHubAPI} params.github - GitHub API instance
* @param {GitHubContext} params.context - GitHub Actions context
* @param {ActionsCore} params.core - GitHub Actions core utilities
* @param {FilesData} params.filesData - Untranslated files data
* @returns {Promise<void>}
*/
export default async ({github, context, core, filesData}) => {
const owner = context.repo.owner;
const repo = context.repo.repo;

core.info(`Processing ${filesData.count} untranslated files...`);

// Translation Checkout ラベルの全Issue (open only) を取得
const { data: checkoutIssues } = await github.rest.issues.listForRepo({
owner,
repo,
state: 'open',
labels: 'type: Translation Checkout'
});

core.info(`Found ${checkoutIssues.length} Translation Checkout issues`);

// Issueタイトルからファイルパスを抽出してマップを作成
// タイトル形式: "{ファイルパス} の翻訳"
// 前方一致でマッチング(ディレクトリ名での宣言に対応)
const checkoutIssuesMap = new Map();
for (const issue of checkoutIssues) {
const match = issue.title.match(/^(.+)\s+の翻訳$/);
if (match) {
const declaredPath = `src/content/${match[1]}`;
// 各未翻訳ファイルに対して前方一致チェック
for (const file of filesData.files) {
if (file.path.startsWith(declaredPath)) {
checkoutIssuesMap.set(file.path, issue.number);
}
}
}
}

core.info(`Mapped ${checkoutIssuesMap.size} files to checkout issues`);

// 既存のトラッキングIssueを検索 (state: all で closed も含む)
const { data: issues } = await github.rest.issues.listForRepo({
owner,
repo,
state: 'all',
labels: LABELS[0],
creator: 'github-actions[bot]'
});

const trackingIssue = issues.find(issue => issue.title === ISSUE_TITLE);

const issueBody = generateIssueBody(filesData, checkoutIssuesMap);

if (trackingIssue) {
core.info(`Found existing tracking issue #${trackingIssue.number}`);

// Issueを更新 (タイトルも更新して新しい形式に移行)
await github.rest.issues.update({
owner,
repo,
issue_number: trackingIssue.number,
title: ISSUE_TITLE,
body: issueBody,
state: 'open' // closed状態の場合はreopen
});

core.info(`Updated tracking issue #${trackingIssue.number}`);

if (trackingIssue.state === 'closed') {
core.info(`Reopened tracking issue #${trackingIssue.number}`);
}
} else {
// 新規Issueを作成
const { data: newIssue } = await github.rest.issues.create({
owner,
repo,
title: ISSUE_TITLE,
body: issueBody,
labels: LABELS
});

core.info(`Created new tracking issue #${newIssue.number}`);
}

core.info('Done!');
};
47 changes: 47 additions & 0 deletions .github/workflows/sync-untranslated-issue.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
name: Sync Untranslated Files Issue

on:
push:
branches:
- main
issues:
types: [opened, closed, reopened, labeled]
workflow_dispatch:

permissions:
contents: read
issues: write

jobs:
update-issue:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
submodules: true

- name: Setup pnpm
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # 4.1.0

- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version-file: '.node-version'
cache: pnpm

- run: pnpm install

- name: Get untranslated files
id: files
run: |
FILES_JSON=$(npx tsx tools/list-untranslated.ts --json)
echo "data<<EOF" >> $GITHUB_OUTPUT
echo "$FILES_JSON" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT

- name: Update tracking issue
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
with:
script: |
const { default: syncIssue } = await import('${{ github.workspace }}/.github/scripts/sync-untranslated-issue.mjs');
const filesData = JSON.parse(`${{ steps.files.outputs.data }}`);
await syncIssue({github, context, core, filesData});
Loading