feat: display multilingual release notes in version management
- Support Simplified Chinese, Traditional Chinese, English, and Japanese - Follow the UI language and show a notice when notes are unavailable - Avoid language and content fallbacks - Generate Release bodies and update manifests from per-version Markdown - Add collapsible notes, publication dates, and links to full releases - Test publishing, legacy manifest compatibility, and localized rendering
This commit is contained in:
1 parent
c9c0c4cf1c
commit
bac67c1524
25 files changed
+888
-9
No files matched your search
@@ -0,0 +1,63 @@
|
||||
import { readFile, writeFile } from 'node:fs/promises';
|
||||
import { join, resolve } from 'node:path';
|
||||
import { pathToFileURL } from 'node:url';
|
||||
import { validateAppVersion } from './version.mjs';
|
||||
|
||||
export const releaseNotesLanguages = [
|
||||
{ locale: 'zh-CN', label: '简体中文', missing: '暂未提供该版本的简体中文更新说明。' },
|
||||
{ locale: 'zh-TW', label: '繁體中文', missing: '暫未提供此版本的繁體中文更新說明。' },
|
||||
{ locale: 'en', label: 'English', missing: 'Release notes for this version are not available in English.' },
|
||||
{ locale: 'ja', label: '日本語', missing: 'このバージョンの日本語の更新内容はありません。' },
|
||||
];
|
||||
|
||||
// Only explicitly authored translations are published. Missing languages stay missing.
|
||||
export async function resolveReleaseNotes({ root = process.cwd(), tag }) {
|
||||
const version = validateAppVersion(String(tag ?? '').trim().replace(/^v/, ''), 'release tag');
|
||||
const directory = join(root, 'docs', 'release-notes', `v${version}`);
|
||||
const notes = {};
|
||||
for (const { locale } of releaseNotesLanguages) {
|
||||
try {
|
||||
const text = (await readFile(join(directory, `${locale}.md`), 'utf8')).trim();
|
||||
if (text) notes[locale] = `${text}\n`;
|
||||
} catch (error) {
|
||||
if (error.code !== 'ENOENT') throw error;
|
||||
}
|
||||
}
|
||||
return notes;
|
||||
}
|
||||
|
||||
export function validateReleaseNotes(notes) {
|
||||
if (!notes || typeof notes !== 'object' || Array.isArray(notes)) {
|
||||
throw new Error('Release notes must be an object keyed by locale');
|
||||
}
|
||||
for (const [locale, text] of Object.entries(notes)) {
|
||||
if (!releaseNotesLanguages.some((language) => language.locale === locale)
|
||||
|| typeof text !== 'string' || !text.trim()) {
|
||||
throw new Error(`Invalid release notes for locale: ${locale}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function releaseNotesBody(notes) {
|
||||
validateReleaseNotes(notes);
|
||||
return `${releaseNotesLanguages.map(({ locale, label, missing }) => (
|
||||
`# ${label}\n\n${notes[locale]?.trim() || missing}`
|
||||
)).join('\n\n---\n\n')}\n`;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const args = new Map();
|
||||
for (let index = 2; index < process.argv.length; index += 2) {
|
||||
args.set(process.argv[index], process.argv[index + 1]);
|
||||
}
|
||||
if (!args.get('--output') || !args.get('--body-output')) {
|
||||
throw new Error('--output and --body-output are required');
|
||||
}
|
||||
const notes = await resolveReleaseNotes({ tag: args.get('--tag') });
|
||||
await writeFile(resolve(args.get('--output')), `${JSON.stringify(notes, null, 2)}\n`, 'utf8');
|
||||
await writeFile(resolve(args.get('--body-output')), releaseNotesBody(notes), 'utf8');
|
||||
}
|
||||
|
||||
if (process.argv[1] && import.meta.url === pathToFileURL(resolve(process.argv[1])).href) {
|
||||
await main();
|
||||
}
|
||||
Reference in new issue
Block a user