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
@@ -3,6 +3,7 @@ import { readFile, stat, writeFile } from 'node:fs/promises';
|
||||
import { basename, join, resolve } from 'node:path';
|
||||
import { pathToFileURL } from 'node:url';
|
||||
import { validateAppVersion } from './version.mjs';
|
||||
import { validateReleaseNotes } from './release-notes.mjs';
|
||||
|
||||
export function portableUpdateManifestName(platform) {
|
||||
const normalizedPlatform = String(platform).trim().toLowerCase();
|
||||
@@ -31,6 +32,7 @@ export async function generatePortableUpdateManifest({
|
||||
gitcodeRepository,
|
||||
tag: rawTag,
|
||||
publishedAt = new Date().toISOString(),
|
||||
releaseNotes,
|
||||
}) {
|
||||
const resolvedDirectory = resolve(directory ?? 'artifacts');
|
||||
const platformSpecs = {
|
||||
@@ -62,6 +64,7 @@ export async function generatePortableUpdateManifest({
|
||||
if (Number.isNaN(Date.parse(publishedAt))) {
|
||||
throw new Error(`Invalid publishedAt: ${publishedAt}`);
|
||||
}
|
||||
if (releaseNotes !== undefined) validateReleaseNotes(releaseNotes);
|
||||
|
||||
const assets = {};
|
||||
for (const arch of ['amd64', 'aarch64']) {
|
||||
@@ -91,6 +94,7 @@ export async function generatePortableUpdateManifest({
|
||||
version,
|
||||
publishedAt,
|
||||
releaseUrl: `https://github.com/${resolvedRepository}/releases/tag/${tag}`,
|
||||
...(releaseNotes === undefined ? {} : { releaseNotes }),
|
||||
assets,
|
||||
};
|
||||
|
||||
@@ -116,6 +120,9 @@ async function main() {
|
||||
? [resolve(requestedOutput)]
|
||||
: portableUpdateManifestNames(platform).map((name) => join(directory, name));
|
||||
const publishedAt = new Date().toISOString();
|
||||
const releaseNotes = args.has('--release-notes')
|
||||
? JSON.parse(await readFile(resolve(args.get('--release-notes')), 'utf8'))
|
||||
: undefined;
|
||||
let manifest;
|
||||
for (const output of outputs) {
|
||||
manifest = await generatePortableUpdateManifest({
|
||||
@@ -126,6 +133,7 @@ async function main() {
|
||||
gitcodeRepository: args.get('--gitcode-repository'),
|
||||
tag: args.get('--tag'),
|
||||
publishedAt,
|
||||
releaseNotes,
|
||||
});
|
||||
}
|
||||
console.log(
|
||||
|
||||
@@ -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