- Updated the introductory notes in both English and Vietnamese deployment guides for clarity. - Changed subgraph titles in Mermaid diagrams for consistency and improved readability. - Added color styling to Mermaid diagram elements for better visual distinction.
257 lines
9.1 KiB
TypeScript
257 lines
9.1 KiB
TypeScript
// 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<string, { order: number; label: { en: string; vi: string } }> = {
|
|
'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<string, string[]> = {
|
|
'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<string, { en: string; vi: string }>();
|
|
|
|
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<string, NavItem[]>();
|
|
|
|
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);
|
|
}
|