// EN: Server-side utility to generate docs navigation from filesystem // VI: Utility phía server để tạo docs navigation từ filesystem import fs from 'fs'; import path from 'path'; import matter from 'gray-matter'; import type { DocsLocale, NavItem, NavGroup } from './docs-types'; interface DocFile { slug: string; title: string; category: string; filePath: string; } // EN: Get absolute path to docs directory // VI: Lấy đường dẫn tuyệt đối tới thư mục docs const DOCS_ROOT = path.join(process.cwd(), '../../docs'); // EN: Category display order and labels // VI: Thứ tự hiển thị và nhãn cho các danh mục const CATEGORY_CONFIG: Record = { 'guides': { order: 1, label: { en: 'Guides', vi: 'Hướng dẫn' } }, 'architecture': { order: 2, label: { en: 'Architecture', vi: 'Kiến trúc' } }, 'skills': { order: 3, label: { en: 'Skills', vi: 'Kỹ năng' } }, 'templates': { order: 4, label: { en: 'Templates', vi: 'Mẫu' } }, 'api': { order: 5, label: { en: 'API Reference', vi: 'Tài liệu API' } }, 'runbooks': { order: 6, label: { en: 'Runbooks', vi: 'Sổ tay vận hành' } }, 'onboarding': { order: 7, label: { en: 'Onboarding', vi: 'Hướng dẫn mới' } }, }; // EN: Custom ordering for specific items within categories // VI: Thứ tự tùy chỉnh cho các items cụ thể trong mỗi category // Items not listed here will be sorted alphabetically after the ordered ones const ITEM_ORDER: Record = { 'guides': [ 'getting-started', // EN: Always show getting started first / VI: Luôn hiển thị getting started đầu tiên 'local-development', // EN: Then local setup / VI: Sau đó là local setup 'development', // EN: Then development / VI: Sau đó là development 'deployment', // EN: Then deployment / VI: Sau đó là deployment // EN: Other items will be alphabetically sorted / VI: Các items khác sẽ được sắp xếp theo alphabet ], 'architecture': [ 'system-design', // EN: Overview first / VI: Overview trước 'service-communication', // EN: Then communication / VI: Sau đó là communication ], 'skills': [ 'project-rules', // EN: Project rules first / VI: Project rules trước 'comment-code', // EN: Then coding standards / VI: Sau đó là coding standards 'api-design', // EN: Then API design / VI: Sau đó là API design ], }; /** * EN: Extract title from markdown file * VI: Trích xuất tiêu đề từ file markdown */ function extractTitle(filePath: string): string { try { const content = fs.readFileSync(filePath, 'utf-8'); // EN: Try to parse frontmatter first // VI: Thử parse frontmatter trước try { const { data } = matter(content); if (data.name) return data.name; if (data.title) return data.title; } catch (e) { // Ignore frontmatter parsing errors } // EN: Fallback to first H1 heading // VI: Fallback về H1 heading đầu tiên const h1Match = content.match(/^#\s+(.+)$/m); if (h1Match) { // EN: Remove bilingual suffix if exists (e.g., "Title / Tiêu đề" -> "Title") // VI: Xóa hậu tố bilingual nếu tồn tại const title = h1Match[1].split('/')[0].trim(); return title; } // EN: Final fallback to filename // VI: Fallback cuối cùng là tên file const filename = path.basename(filePath, '.md'); return filename .split('-') .map(word => word.charAt(0).toUpperCase() + word.slice(1)) .join(' '); } catch (error) { // EN: If file read fails, use filename // VI: Nếu đọc file thất bại, dùng tên file const filename = path.basename(filePath, '.md'); return filename .split('-') .map(word => word.charAt(0).toUpperCase() + word.slice(1)) .join(' '); } } /** * EN: Scan docs directory for a specific locale * VI: Quét thư mục docs cho một locale cụ thể */ function scanDocsForLocale(locale: DocsLocale): DocFile[] { const localeDir = path.join(DOCS_ROOT, locale); const docs: DocFile[] = []; if (!fs.existsSync(localeDir)) { return docs; } // EN: Read all category directories // VI: Đọc tất cả thư mục danh mục const categories = fs.readdirSync(localeDir, { withFileTypes: true }) .filter(dirent => dirent.isDirectory()) .map(dirent => dirent.name); for (const category of categories) { const categoryPath = path.join(localeDir, category); const files = fs.readdirSync(categoryPath) .filter(file => file.endsWith('.md') && file !== 'README.md'); for (const file of files) { const filePath = path.join(categoryPath, file); const slug = `${category}/${path.basename(file, '.md')}`; const title = extractTitle(filePath); docs.push({ slug, title, category, filePath }); } } return docs; } /** * EN: Generate navigation structure with bilingual support * VI: Tạo cấu trúc navigation với hỗ trợ đa ngôn ngữ */ export function generateDocsNavigation(): NavGroup[] { const enDocs = scanDocsForLocale('en'); const viDocs = scanDocsForLocale('vi'); // EN: Create a map of slug to titles in both languages // VI: Tạo map từ slug tới tiêu đề ở cả hai ngôn ngữ const docTitles = new Map(); enDocs.forEach(doc => { docTitles.set(doc.slug, { en: doc.title, vi: doc.title // Default to EN if VI not found }); }); viDocs.forEach(doc => { const existing = docTitles.get(doc.slug); if (existing) { existing.vi = doc.title; } else { docTitles.set(doc.slug, { en: doc.title, vi: doc.title }); } }); // EN: Group by category // VI: Nhóm theo danh mục const groups = new Map(); docTitles.forEach((labels, slug) => { const category = slug.split('/')[0]; if (!groups.has(category)) { groups.set(category, []); } groups.get(category)!.push({ id: slug.replace(/\//g, '-'), slug, label: labels }); }); // EN: Convert to NavGroup array with sorting // VI: Chuyển thành mảng NavGroup với sắp xếp const navGroups: NavGroup[] = []; groups.forEach((items, category) => { const config = CATEGORY_CONFIG[category] || { order: 999, label: { en: category.charAt(0).toUpperCase() + category.slice(1), vi: category.charAt(0).toUpperCase() + category.slice(1) } }; // EN: Sort items by custom order first, then alphabetically // VI: Sắp xếp items theo thứ tự tùy chỉnh trước, sau đó theo alphabet const customOrder = ITEM_ORDER[category] || []; items.sort((a, b) => { const aSlugName = a.slug.split('/')[1]; // EN: Get filename from slug / VI: Lấy filename từ slug const bSlugName = b.slug.split('/')[1]; const aIndex = customOrder.indexOf(aSlugName); const bIndex = customOrder.indexOf(bSlugName); // EN: If both have custom order, use that / VI: Nếu cả hai có thứ tự tùy chỉnh, dùng nó if (aIndex !== -1 && bIndex !== -1) { return aIndex - bIndex; } // EN: Items with custom order come first / VI: Items có thứ tự tùy chỉnh xuất hiện trước if (aIndex !== -1) return -1; if (bIndex !== -1) return 1; // EN: Otherwise sort alphabetically / VI: Ngược lại sắp xếp theo alphabet return a.label.en.localeCompare(b.label.en); }); navGroups.push({ id: category, label: config.label, items }); }); // EN: Sort groups by order // VI: Sắp xếp groups theo thứ tự navGroups.sort((a, b) => { const orderA = CATEGORY_CONFIG[a.id]?.order || 999; const orderB = CATEGORY_CONFIG[b.id]?.order || 999; return orderA - orderB; }); return navGroups; } /** * EN: Get list of available doc slugs for a locale * VI: Lấy danh sách slugs có sẵn cho một locale */ export function getAvailableDocs(locale: DocsLocale): string[] { const docs = scanDocsForLocale(locale); return docs.map(doc => doc.slug); } /** * EN: Check if a doc exists for a locale * VI: Kiểm tra xem một doc có tồn tại cho locale không */ export function docExists(locale: DocsLocale, slug: string): boolean { const slugPath = slug.replace(/\//g, path.sep); const filePath = path.join(DOCS_ROOT, locale, `${slugPath}.md`); return fs.existsSync(filePath); }