fix(validate-sidebar): tighten language detection and drift guard, add docstrings

* fix(validate-sidebar): replace subdirectory heuristic with locale pattern matching

detectLanguageDirs() previously classified any top-level docs/ directory
containing subdirectories as a translation language. This was too broad —
if an English section ever gained nested subfolders it would be silently
excluded from validation.

Replaced with a BCP 47 locale-code regex (/^[a-z]{2}(?:-[a-zA-Z]{2})?$/)
that matches known patterns (cs, fr, vi-vn, zh-cn) and won't falsely
classify content sections like explanation/ or reference/.

* fix(validate-sidebar): guard drift check against undefined order values

extractSidebarOrder() returns { hasSidebar: false } when no sidebar block
exists, leaving order as undefined rather than null. The drift check only
guarded against null, allowing undefined values to emit noisy warnings
like "Order drift: ... order undefined".

Changed the guard to typeof === 'number' which correctly excludes both
undefined and null without relying on a specific sentinel value.

* chore(validate-sidebar): add JSDoc docstrings to all functions

Adds @param and @returns annotations to extractSidebarOrder,
detectLanguageDirs, getEnglishSections, checkDirectory,
checkTranslationDrift, and relativePath.
This commit is contained in:
Emmanuel Atsé 2026-05-22 02:29:49 +02:00
parent 98bf8a3a9f
commit 7ef3a66dca
No known key found for this signature in database
1 changed files with 38 additions and 15 deletions

View File

@ -23,6 +23,13 @@ const DOCS_ROOT = path.resolve(__dirname, '../docs');
const FRONTMATTER_REGEX = /^---\r?\n([\s\S]*?)\r?\n---/;
const SIDEBAR_ORDER_REGEX = /sidebar:\s*\r?\n(?:(?:[ \t]+.*\r?\n)|(?:\r?\n))*?[ \t]+order:\s*(\d+)/;
const HAS_SIDEBAR_REGEX = /^sidebar:/m;
const LOCALE_PATTERN = /^[a-z]{2}(?:-[a-zA-Z]{2})?$/;
/**
* Extract sidebar.order from YAML frontmatter.
* @param {string} content - Full file contents of a markdown file.
* @returns {{ hasSidebar: boolean, order?: number|null }} Whether a sidebar block exists and its order value.
*/
function extractSidebarOrder(content) {
const match = content.match(FRONTMATTER_REGEX);
if (!match) return { hasSidebar: false };
@ -41,29 +48,33 @@ function extractSidebarOrder(content) {
return { hasSidebar: true, order: parseInt(orderMatch[1], 10) };
}
/**
* Detect translation language directories under docs/ by matching locale-code names.
* @returns {string[]} Directory names matching locale patterns (e.g. "cs", "zh-cn").
*/
function detectLanguageDirs() {
const dirs = [];
const entries = fs.readdirSync(DOCS_ROOT, { withFileTypes: true });
for (const entry of entries) {
if (!entry.isDirectory() || entry.name.startsWith('_')) continue;
const subPath = path.join(DOCS_ROOT, entry.name);
const subEntries = fs.readdirSync(subPath, { withFileTypes: true });
const hasSubdirs = subEntries.some((e) => e.isDirectory() && !e.name.startsWith('_'));
if (hasSubdirs) {
dirs.push(entry.name);
}
}
return dirs;
return entries
.filter((e) => e.isDirectory() && !e.name.startsWith('_'))
.map((e) => e.name)
.filter((name) => LOCALE_PATTERN.test(name));
}
/**
* List all top-level content directories under docs/ (excludes _-prefixed dirs).
* @returns {string[]} Directory names for English content sections.
*/
function getEnglishSections() {
const entries = fs.readdirSync(DOCS_ROOT, { withFileTypes: true });
return entries.filter((e) => e.isDirectory() && !e.name.startsWith('_')).map((e) => e.name);
}
/**
* Validate sidebar.order values for all markdown files in a directory.
* @param {string} dirPath - Absolute path to the directory to scan.
* @param {Array<{level:string,type:string,file?:string,order?:number,directory?:string,missing?:number,message:string}>} issues - Array to push errors into.
* @returns {Map<number,string[]>} Map of order values to the files that hold them.
*/
function checkDirectory(dirPath, issues) {
const entries = fs.readdirSync(dirPath, { withFileTypes: true });
const mdFiles = entries.filter((e) => e.isFile() && (e.name.endsWith('.md') || e.name.endsWith('.mdx')));
@ -132,6 +143,13 @@ function checkDirectory(dirPath, issues) {
return orderMap;
}
/**
* Compare translated sidebar orders against English counterparts and warn on drift.
* @param {string} lang - Language directory name (e.g. "cs").
* @param {string[]} langSections - Section subdirectories within the language folder.
* @param {Map<string,Map<number,string[]>>} englishOrderMaps - English order maps keyed by section name.
* @param {Array<{level:string,type:string,file:string,englishFile:string,langOrder:number,englishOrder:number,message:string}>} warnings - Array to push drift warnings into.
*/
function checkTranslationDrift(lang, langSections, englishOrderMaps, warnings) {
for (const section of langSections) {
const sectionDir = path.join(DOCS_ROOT, lang, section);
@ -155,7 +173,7 @@ function checkTranslationDrift(lang, langSections, englishOrderMaps, warnings) {
const langResult = extractSidebarOrder(langContent);
const engResult = extractSidebarOrder(engContent);
if (langResult.order !== null && engResult.order !== null && langResult.order !== engResult.order) {
if (typeof langResult.order === 'number' && typeof engResult.order === 'number' && langResult.order !== engResult.order) {
warnings.push({
level: 'warning',
type: 'order-drift',
@ -170,6 +188,11 @@ function checkTranslationDrift(lang, langSections, englishOrderMaps, warnings) {
}
}
/**
* Convert an absolute path to one relative to DOCS_ROOT.
* @param {string} filePath - Absolute file path.
* @returns {string} Relative path from docs root.
*/
function relativePath(filePath) {
return path.relative(DOCS_ROOT, filePath);
}