Files
MyIP/tests/changelog.test.js
jason5ng32andClaude Fable 5 6301d8b838 Feat(i18n): beta locale support with fallback chains, pack gate and status dashboard
A locale can now ship partially translated: registry status drives a Beta
badge in the picker, missing keys walk variant -> base -> en (one chain
definition feeds vue-i18n, the privacy/checklist datasets and the
changelog), and matchLocale gained family matching so pt-PT finds a
pt-BR pack. Changelog history is only required of full locales, removing
the biggest barrier for new-language PRs. tests/locale-packs.test.js is
the hard gate a pack must pass; scripts/i18n-status.js (un-gitignored
for contributors) reports coverage without ever failing. Only visible
behavior change: ?hl= resolves regional tags (?hl=zh-CN -> zh) instead
of dropping to English.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-22 20:13:14 +08:00

63 lines
3.2 KiB
JavaScript

// Test changelog.json format and content
// Verifies the format and content of changelog.json
// In case forgetting to update all locales, this test will fail.
import assert from 'node:assert/strict';
import { describe, it } from 'node:test';
import fs from 'node:fs';
import changelog from '../frontend/data/changelog.json' with { type: 'json' };
import { FULL_LOCALE_CODES } from '../common/locale-registry.js';
// Beta locales are exempt: back-translating the whole history is the single
// biggest deterrent to a first translation PR.
const REQUIRED_LOCALES = FULL_LOCALE_CODES;
const VALID_TYPES = new Set(['add', 'improve', 'fix']);
describe('changelog.json', () => {
it('is a non-empty array', () => {
assert.ok(Array.isArray(changelog), 'changelog root must be an array');
assert.ok(changelog.length > 0, 'changelog must contain at least one version');
});
it('every version has { version, date, content[] }', () => {
for (const v of changelog) {
assert.equal(typeof v.version, 'string', `missing version string`);
assert.match(v.version, /^v\d+\.\d+/, `version ${v.version} should look like "vX.Y"`);
assert.equal(typeof v.date, 'string', `${v.version} missing date`);
// ISO dates localize per viewer in the Footer; "Beta" marks the unreleased entry.
assert.match(v.date, /^(\d{4}-\d{2}-\d{2}|Beta)$/,
`${v.version} date must be ISO "YYYY-MM-DD" (or "Beta"), got "${v.date}"`);
assert.ok(Array.isArray(v.content), `${v.version} content must be an array`);
assert.ok(v.content.length > 0, `${v.version} content must be non-empty`);
}
});
it('every content item has a valid type and a complete change object', () => {
for (const v of changelog) {
v.content.forEach((item, idx) => {
const label = `${v.version} item#${idx}`;
assert.ok(VALID_TYPES.has(item.type), `${label} type must be one of ${[...VALID_TYPES]}, got ${item.type}`);
assert.equal(typeof item.change, 'object', `${label} change must be an object`);
assert.ok(item.change !== null, `${label} change must not be null`);
for (const lang of REQUIRED_LOCALES) {
assert.equal(typeof item.change[lang], 'string', `${label} missing ${lang} translation`);
assert.ok(item.change[lang].length > 0, `${label} ${lang} translation is empty`);
}
});
}
});
it('locale files no longer carry changelog.versions (migrated to changelog.json)', () => {
for (const lang of REQUIRED_LOCALES) {
const pack = JSON.parse(fs.readFileSync(`frontend/locales/${lang}.json`, 'utf8'));
assert.equal(pack.changelog?.versions, undefined,
`${lang}.json still has changelog.versions — it should live only in changelog.json`);
// UI chrome should still be present.
for (const key of ['Title', 'add', 'improve', 'fix']) {
assert.equal(typeof pack.changelog?.[key], 'string',
`${lang}.json lost changelog.${key} during migration`);
}
}
});
});