From 7ef3a66dca3dce93c2fb8121f23c80f4f1415059 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emmanuel=20Ats=C3=A9?= <4688434+eatse21@users.noreply.github.com> Date: Fri, 22 May 2026 02:29:49 +0200 Subject: [PATCH] fix(validate-sidebar): tighten language detection and drift guard, add docstrings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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. --- tools/validate-sidebar-order.js | 53 +++++++++++++++++++++++---------- 1 file changed, 38 insertions(+), 15 deletions(-) diff --git a/tools/validate-sidebar-order.js b/tools/validate-sidebar-order.js index fe3950459..c08b57e2e 100644 --- a/tools/validate-sidebar-order.js +++ b/tools/validate-sidebar-order.js @@ -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} 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>} 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); }