博客

深入 @lunariajs/core 源码:理解 LunariaJS 的核心架构

深入剖析 LunariaJS 核心库的架构设计和实现原理,从源码层面理解文件解析、状态追踪、配置处理等核心模块,为自定义扩展和高级用法打下基础。

LibDoc Team 2026年3月6日 LunariaJS 专栏 131 分钟阅读
#LunariaJS #源码解析 #核心架构 #@lunariajs/core #TypeScript

深入 @lunariajs/core 源码:理解 LunariaJS 的核心架构

在前面七篇入门文章中,我们全面学习了 LunariaJS 的安装、配置、CLI 命令、仪表板、Git 工作流、Starlight 集成和 CI/CD 集成。现在,让我们深入源码层面,理解 LunariaJS 的核心架构和实现原理。

💡 官方文档LunariaJS 中文文档 - API 参考

核心架构概览

整体架构图

LunariaJS 的核心架构可以概括为以下层次:

┌─────────────────────────────────────────────────────────────────┐
│                        CLI Layer                                 │
│                   (lunaria init/build/preview)                   │
├─────────────────────────────────────────────────────────────────┤
│                       API Layer                                  │
│              (Public API for external use)                       │
├─────────────────────────────────────────────────────────────────┤
│                     Core Modules                                 │
│  ┌──────────────┐ ┌──────────────┐ ┌──────────────┐             │
│  │   Config     │ │   File       │ │   Status     │             │
│  │   System     │ │   Parsers    │ │   Tracker    │             │
│  └──────────────┘ └──────────────┘ └──────────────┘             │
│  ┌──────────────┐ ┌──────────────┐ ┌──────────────┐             │
│  │   Git        │ │   Dashboard  │ │   Reporter   │             │
│  │   Client     │ │   Generator  │ │              │             │
│  └──────────────┘ └──────────────┘ └──────────────┘             │
├─────────────────────────────────────────────────────────────────┤
│                     Infrastructure                               │
│            (File System, Git, Logger, Utils)                     │
└─────────────────────────────────────────────────────────────────┘

核心模块职责

模块职责核心文件
Config System配置加载、验证、默认值处理config.ts
File Parsers解析不同格式的本地化文件parsers/
Status Tracker追踪翻译状态(done/outdated/missing)status.ts
Git ClientGit 操作封装(获取提交历史、文件状态)git.ts
Dashboard Generator生成静态 HTML 仪表板dashboard/
Reporter生成状态报告和日志输出reporter.ts

技术栈选择

LunariaJS 选择以下技术栈:

技术用途选择原因
TypeScript主要语言类型安全,更好的 IDE 支持
ZodSchema 验证配置文件验证
Vite构建工具快速的 ES 模块支持
simple-gitGit 操作Node.js Git 客户端
handlebars模板引擎仪表板 HTML 生成

目录结构

@lunariajs/core/
├── src/
│   ├── index.ts              # 公开 API 入口
│   ├── config.ts             # 配置系统
│   ├── status.ts             # 状态追踪
│   ├── git.ts                # Git 操作
│   ├── reporter.ts           # 报告生成
│   ├── parsers/              # 文件解析器
│   │   ├── index.ts
│   │   ├── json.ts
│   │   ├── yaml.ts
│   │   └── markdown.ts
│   ├── dashboard/            # 仪表板生成
│   │   ├── index.ts
│   │   ├── templates/
│   │   └── assets/
│   ├── types/                # 类型定义
│   │   ├── config.ts
│   │   └── status.ts
│   └── utils/                # 工具函数
│       ├── file.ts
│       └── logger.ts
├── package.json
├── tsconfig.json
└── README.md

配置系统

配置类型定义

LunariaJS 使用 TypeScript 定义严格的配置类型:

// src/types/config.ts

/**
 * 支持的文件格式
 */
export type FileFormat = 'json' | 'yaml' | 'md' | 'csv';

/**
 * 语言配置
 */
export interface LocaleConfig {
  /** 语言标签(如 'en', 'zh-cn') */
  label: string;
  /** 语言方向 */
  dir?: 'ltr' | 'rtl';
  /** 自定义数据 */
  [key: string]: unknown;
}

/**
 * 文件模式配置
 */
export interface FilePattern {
  /** 源语言文件路径模式 */
  sourcePath: string;
  /** 翻译文件路径模式 */
  localizationPath: string;
  /** 包含的文件 glob 模式 */
  include?: string[];
  /** 排除的文件 glob 模式 */
  exclude?: string[];
}

/**
 * 仪表板配置
 */
export interface DashboardConfig {
  /** 输出目录 */
  outputDir: string;
  /** 仪表板标题 */
  title?: string;
  /** 仪表板描述 */
  description?: string;
  /** 界面语言 */
  uiLanguage?: string;
  /** 自定义 CSS 路径 */
  customCss?: string;
  /** 站点 URL(用于 SEO) */
  site?: string;
}

/**
 * LunariaJS 完整配置
 */
export interface LunariaConfig {
  /** 源语言 */
  sourceLanguage: string;
  /** 所有支持的语言 */
  languages: string[];
  /** 语言配置映射 */
  locales?: Record<string, LocaleConfig>;
  /** 文件模式列表 */
  files: FilePattern[];
  /** 仪表板配置 */
  dashboard?: DashboardConfig;
  /** 忽略的关键词(用于检测占位符翻译) */
  ignoreKeywords?: string[];
}

配置加载机制

配置加载过程分为三个阶段:

// src/config.ts

import { promises as fs } from 'fs';
import { pathExists } from './utils/file';
import { LunariaConfig } from './types/config';
import { defaultConfig } from './config/defaults';

/**
 * 配置加载器
 */
export async function loadConfig(
  configPath: string = 'lunaria.config.json'
): Promise<LunariaConfig> {
  // 阶段 1: 尝试加载用户配置
  const userConfig = await loadUserConfig(configPath);

  // 阶段 2: 合并默认配置
  const mergedConfig = mergeConfig(defaultConfig, userConfig);

  // 阶段 3: 验证配置
  const validatedConfig = validateConfig(mergedConfig);

  return validatedConfig;
}

/**
 * 加载用户配置文件
 */
async function loadUserConfig(
  configPath: string
): Promise<Partial<LunariaConfig>> {
  // 支持 JSON 和 JS/TS 格式
  if (configPath.endsWith('.json')) {
    return loadJsonConfig(configPath);
  } else if (configPath.endsWith('.js') || configPath.endsWith('.ts')) {
    return loadJsConfig(configPath);
  }

  // 默认尝试 JSON
  return loadJsonConfig(configPath);
}

/**
 * 加载 JSON 配置
 */
async function loadJsonConfig(
  configPath: string
): Promise<Partial<LunariaConfig>> {
  if (!(await pathExists(configPath))) {
    return {};
  }

  const content = await fs.readFile(configPath, 'utf-8');
  return JSON.parse(content);
}

/**
 * 加载 JavaScript/TypeScript 配置
 */
async function loadJsConfig(
  configPath: string
): Promise<Partial<LunariaConfig>> {
  // 动态导入 ES 模块
  const module = await import(require.resolve(configPath, { paths: [process.cwd()] }));
  return module.default || module;
}

/**
 * 合并配置(深度合并)
 */
function mergeConfig(
  defaultCfg: LunariaConfig,
  userCfg: Partial<LunariaConfig>
): LunariaConfig {
  return {
    ...defaultCfg,
    ...userCfg,
    dashboard: {
      ...defaultCfg.dashboard,
      ...userCfg.dashboard,
    },
  };
}

配置验证

使用 Zod 进行严格的配置验证:

// src/config/validation.ts

import { z } from 'zod';

/**
 * 配置 Schema 定义
 */
const LunariaConfigSchema = z.object({
  sourceLanguage: z.string().min(1, 'sourceLanguage is required'),
  languages: z.array(z.string()).min(1, 'At least one language is required'),
  files: z.array(
    z.object({
      sourcePath: z.string(),
      localizationPath: z.string(),
      include: z.array(z.string()).optional(),
      exclude: z.array(z.string()).optional(),
    })
  ).min(1, 'At least one file pattern is required'),
  dashboard: z.object({
    outputDir: z.string().default('lunaria-dashboard'),
    title: z.string().optional(),
    description: z.string().optional(),
  }).optional(),
});

/**
 * 验证配置
 */
export function validateConfig(config: unknown): LunariaConfig {
  const result = LunariaConfigSchema.safeParse(config);

  if (!result.success) {
    const errors = result.error.errors.map(
      (e) => `${e.path.join('.')}: ${e.message}`
    );
    throw new Error(`Invalid configuration:\n${errors.join('\n')}`);
  }

  // 额外的语义验证
  validateSemantics(result.data);

  return result.data;
}

/**
 * 语义验证
 */
function validateSemantics(config: LunariaConfig): void {
  // 检查源语言是否在语言列表中
  if (!config.languages.includes(config.sourceLanguage)) {
    throw new Error(
      `sourceLanguage "${config.sourceLanguage}" must be included in languages array`
    );
  }

  // 检查路径模式中的占位符
  for (const pattern of config.files) {
    validatePathPattern(pattern.sourcePath, 'sourcePath');
    validatePathPattern(pattern.localizationPath, 'localizationPath');
  }
}

/**
 * 验证路径模式
 */
function validatePathPattern(pattern: string, fieldName: string): void {
  // 必须包含 {slug} 占位符
  if (!pattern.includes('{slug}')) {
    console.warn(
      `Warning: ${fieldName} "${pattern}" does not contain {slug} placeholder. ` +
      `This may result in incorrect file matching.`
    );
  }

  // localizationPath 必须包含 {lang} 占位符
  if (fieldName === 'localizationPath' && !pattern.includes('{lang}')) {
    throw new Error(
      `localizationPath "${pattern}" must contain {lang} placeholder`
    );
  }
}

文件解析器

解析器接口设计

LunariaJS 定义了统一的解析器接口:

// src/parsers/types.ts

/**
 * 本地化文件内容
 */
export interface LocalizedContent {
  /** 文件路径 */
  path: string;
  /** 语言代码 */
  lang: string;
  /** 键值对内容 */
  entries: Record<string, string>;
  /** 原始内容 */
  raw: string;
  /** 最后修改时间 */
  lastModified?: Date;
}

/**
 * 文件解析器接口
 */
export interface FileParser {
  /** 支持的文件扩展名 */
  extensions: string[];

  /** 解析文件内容 */
  parse(content: string, path: string): Promise<LocalizedContent>;

  /** 序列化内容(用于生成文件) */
  stringify(data: Record<string, string>): string;
}

JSON 解析器

// src/parsers/json.ts

import { FileParser, LocalizedContent } from './types';

export const JsonParser: FileParser = {
  extensions: ['.json'],

  async parse(content: string, path: string): Promise<LocalizedContent> {
    try {
      const data = JSON.parse(content);

      // 扁平化嵌套对象
      const entries = flattenObject(data);

      return {
        path,
        lang: extractLangFromPath(path),
        entries,
        raw: content,
      };
    } catch (error) {
      throw new Error(`Failed to parse JSON file ${path}: ${error}`);
    }
  },

  stringify(data: Record<string, string>): string {
    // 将扁平结构还原为嵌套对象
    const nested = unflattenObject(data);
    return JSON.stringify(nested, null, 2);
  },
};

/**
 * 扁平化嵌套对象
 * 例如: { a: { b: 'value' } } => { 'a.b': 'value' }
 */
function flattenObject(obj: Record<string, any>, prefix = ''): Record<string, string> {
  const result: Record<string, string> = {};

  for (const [key, value] of Object.entries(obj)) {
    const newKey = prefix ? `${prefix}.${key}` : key;

    if (typeof value === 'string') {
      result[newKey] = value;
    } else if (typeof value === 'object' && value !== null) {
      Object.assign(result, flattenObject(value, newKey));
    }
  }

  return result;
}

/**
 * 从路径中提取语言代码
 */
function extractLangFromPath(path: string): string {
  const match = path.match(/\/([a-z]{2}(-[a-z]{2})?)\//i);
  return match ? match[1] : 'unknown';
}

YAML 解析器

// src/parsers/yaml.ts

import { FileParser, LocalizedContent } from './types';
import * as yaml from 'js-yaml';

export const YamlParser: FileParser = {
  extensions: ['.yaml', '.yml'],

  async parse(content: string, path: string): Promise<LocalizedContent> {
    try {
      const data = yaml.load(content);

      if (typeof data !== 'object' || data === null) {
        throw new Error('YAML content must be an object');
      }

      const entries = flattenObject(data as Record<string, any>);

      return {
        path,
        lang: extractLangFromPath(path),
        entries,
        raw: content,
      };
    } catch (error) {
      throw new Error(`Failed to parse YAML file ${path}: ${error}`);
    }
  },

  stringify(data: Record<string, string>): string {
    const nested = unflattenObject(data);
    return yaml.dump(nested, { indent: 2, lineWidth: -1 });
  },
};

Markdown 解析器

Markdown 文件的处理略有不同,因为它不是简单的键值对:

// src/parsers/markdown.ts

import { FileParser, LocalizedContent } from './types';
import { matter } from 'gray-matter';

export const MarkdownParser: FileParser = {
  extensions: ['.md', '.mdx'],

  async parse(content: string, path: string): Promise<LocalizedContent> {
    try {
      // 解析 frontmatter
      const { data: frontmatter, content: body } = matter(content);

      // 将 frontmatter 和 body 作为不同的条目
      const entries: Record<string, string> = {
        // frontmatter 条目
        ...flattenObject(frontmatter || {}),
        // body 作为一个整体条目
        '_body': body.trim(),
      };

      return {
        path,
        lang: extractLangFromPath(path),
        entries,
        raw: content,
      };
    } catch (error) {
      throw new Error(`Failed to parse Markdown file ${path}: ${error}`);
    }
  },

  stringify(data: Record<string, string>): string {
    const { _body, ...frontmatter } = data;
    const nested = unflattenObject(frontmatter);

    const frontmatterStr = Object.keys(nested).length > 0
      ? matter.stringify(_body || '', nested)
      : _body || '';

    return frontmatterStr;
  },
};

解析器注册和选择

// src/parsers/index.ts

import { FileParser } from './types';
import { JsonParser } from './json';
import { YamlParser } from './yaml';
import { MarkdownParser } from './markdown';

/**
 * 解析器注册表
 */
const parsers: FileParser[] = [
  JsonParser,
  YamlParser,
  MarkdownParser,
];

/**
 * 根据文件扩展名获取解析器
 */
export function getParser(filePath: string): FileParser | null {
  const ext = filePath.toLowerCase();

  for (const parser of parsers) {
    if (parser.extensions.some(e => ext.endsWith(e))) {
      return parser;
    }
  }

  return null;
}

/**
 * 解析文件
 */
export async function parseFile(
  filePath: string,
  content: string
): Promise<LocalizedContent> {
  const parser = getParser(filePath);

  if (!parser) {
    // 默认使用文本解析器
    return {
      path: filePath,
      lang: extractLangFromPath(filePath),
      entries: { '_raw': content },
      raw: content,
    };
  }

  return parser.parse(content, filePath);
}

状态追踪引擎

状态计算核心逻辑

状态追踪是 LunariaJS 的核心功能:

// src/status.ts

import { LunariaConfig } from './types/config';
import { FileStatus, TranslationStatus } from './types/status';
import { getFileLastModified } from './git';

/**
 * 计算单个文件的翻译状态
 */
export async function calculateFileStatus(
  sourcePath: string,
  translationPath: string,
  lang: string,
  config: LunariaConfig
): Promise<FileStatus> {
  // 检查翻译文件是否存在
  const translationExists = await fileExists(translationPath);

  if (!translationExists) {
    return {
      path: translationPath,
      lang,
      status: 'missing',
      sourcePath,
    };
  }

  // 获取最后修改时间
  const sourceLastModified = await getFileLastModified(sourcePath);
  const translationLastModified = await getFileLastModified(translationPath);

  // 比较时间戳
  if (translationLastModified >= sourceLastModified) {
    return {
      path: translationPath,
      lang,
      status: 'done',
      sourcePath,
      lastModified: translationLastModified,
    };
  }

  // 翻译过时
  const daysOutdated = Math.floor(
    (sourceLastModified.getTime() - translationLastModified.getTime()) /
      (1000 * 60 * 60 * 24)
  );

  return {
    path: translationPath,
    lang,
    status: 'outdated',
    sourcePath,
    lastModified: translationLastModified,
    sourceLastModified,
    daysOutdated,
  };
}

/**
 * 计算所有文件的翻译状态
 */
export async function calculateAllStatus(
  config: LunariaConfig
): Promise<TranslationStatus> {
  const status: TranslationStatus = {
    sourceLanguage: config.sourceLanguage,
    languages: {},
    totalFiles: 0,
    generatedAt: new Date(),
  };

  // 初始化语言状态
  for (const lang of config.languages) {
    status.languages[lang] = {
      total: 0,
      done: 0,
      outdated: 0,
      missing: 0,
      files: [],
    };
  }

  // 遍历所有文件模式
  for (const pattern of config.files) {
    const sourceFiles = await globFiles(pattern.sourcePath, pattern.include, pattern.exclude);

    for (const sourceFile of sourceFiles) {
      status.totalFiles++;
      const slug = extractSlug(sourceFile, pattern.sourcePath);

      // 计算每种目标语言的状态
      for (const lang of config.languages) {
        if (lang === config.sourceLanguage) continue;

        const translationPath = resolveTranslationPath(
          pattern.localizationPath,
          lang,
          slug
        );

        const fileStatus = await calculateFileStatus(
          sourceFile,
          translationPath,
          lang,
          config
        );

        status.languages[lang].total++;
        status.languages[lang].files.push(fileStatus);

        switch (fileStatus.status) {
          case 'done':
            status.languages[lang].done++;
            break;
          case 'outdated':
            status.languages[lang].outdated++;
            break;
          case 'missing':
            status.languages[lang].missing++;
            break;
        }
      }
    }
  }

  return status;
}

/**
 * 计算翻译完成度百分比
 */
export function calculateProgress(status: TranslationStatus, lang: string): number {
  const langStatus = status.languages[lang];
  if (!langStatus || langStatus.total === 0) return 0;

  return Math.round((langStatus.done / langStatus.total) * 100);
}

Git 操作封装

// src/git.ts

import simpleGit, { SimpleGit } from 'simple-git';

let gitClient: SimpleGit | null = null;

/**
 * 获取 Git 客户端实例
 */
function getGitClient(): SimpleGit {
  if (!gitClient) {
    gitClient = simpleGit();
  }
  return gitClient;
}

/**
 * 获取文件的最后修改时间
 */
export async function getFileLastModified(filePath: string): Promise<Date> {
  const git = getGitClient();

  try {
    // 获取文件最后修改的提交
    const log = await git.log(['-1', '--format=%ct', '--', filePath]);

    if (log.latest) {
      const timestamp = parseInt(log.latest.hash, 10);
      return new Date(timestamp * 1000);
    }

    // 如果没有 Git 历史,使用文件系统时间
    const stats = await fs.stat(filePath);
    return stats.mtime;
  } catch (error) {
    // Git 操作失败,使用文件系统时间
    const stats = await fs.stat(filePath);
    return stats.mtime;
  }
}

/**
 * 获取文件的完整提交历史
 */
export async function getFileHistory(
  filePath: string,
  limit: number = 10
): Promise<FileCommit[]> {
  const git = getGitClient();

  try {
    const log = await git.log(['-' + limit, '--format=%H|%ct|%an|%s', '--', filePath]);

    return log.all.map(commit => {
      const [hash, timestamp, author, message] = commit.hash.split('|');
      return {
        hash,
        date: new Date(parseInt(timestamp, 10) * 1000),
        author,
        message,
      };
    });
  } catch (error) {
    return [];
  }
}

/**
 * 检查是否是 Git 仓库
 */
export async function isGitRepository(): Promise<boolean> {
  const git = getGitClient();

  try {
    await git.status();
    return true;
  } catch {
    return false;
  }
}

仪表板生成器

模板系统

LunariaJS 使用 Handlebars 模板引擎生成仪表板:

// src/dashboard/generator.ts

import Handlebars from 'handlebars';
import { TranslationStatus } from '../types/status';
import { readTemplate, copyAssets } from './utils';

// 注册 Handlebars 助手
Handlebars.registerHelper('progressBar', (progress: number) => {
  const color = progress >= 90 ? 'green' : progress >= 70 ? 'yellow' : 'red';
  return new Handlebars.SafeString(
    `<div class="progress-bar">
      <div class="progress-fill ${color}" style="width: ${progress}%"></div>
    </div>`
  );
});

Handlebars.registerHelper('statusIcon', (status: string) => {
  const icons: Record<string, string> = {
    done: '✅',
    outdated: '⚠️',
    missing: '❌',
  };
  return icons[status] || '❓';
});

Handlebars.registerHelper('formatDate', (date: Date) => {
  return new Intl.DateTimeFormat('en-US', {
    year: 'numeric',
    month: 'short',
    day: 'numeric',
  }).format(new Date(date));
});

/**
 * 生成仪表板 HTML
 */
export async function generateDashboard(
  status: TranslationStatus,
  outputDir: string
): Promise<void> {
  // 读取模板
  const indexTemplate = await readTemplate('index.html');
  const compiledTemplate = Handlebars.compile(indexTemplate);

  // 准备模板数据
  const templateData = {
    title: 'Localization Status',
    generatedAt: new Date().toISOString(),
    sourceLanguage: status.sourceLanguage,
    languages: Object.entries(status.languages).map(([lang, data]) => ({
      code: lang,
      total: data.total,
      done: data.done,
      outdated: data.outdated,
      missing: data.missing,
      progress: Math.round((data.done / data.total) * 100),
      files: data.files,
    })),
    totalFiles: status.totalFiles,
  };

  // 渲染 HTML
  const html = compiledTemplate(templateData);

  // 写入文件
  await fs.writeFile(path.join(outputDir, 'index.html'), html);

  // 复制静态资源
  await copyAssets(outputDir);

  // 写入 JSON 数据(供 API 使用)
  await fs.writeFile(
    path.join(outputDir, 'data', 'status.json'),
    JSON.stringify(status, null, 2)
  );
}

模板示例

<!-- src/dashboard/templates/index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>{{title}}</title>
  <link rel="stylesheet" href="assets/styles.css">
</head>
<body>
  <header>
    <h1>🌙 {{title}}</h1>
    <p class="subtitle">Generated at {{formatDate generatedAt}}</p>
  </header>

  <main>
    <section class="overview">
      <h2>Translation Progress</h2>
      <div class="language-grid">
        {{#each languages}}
        <div class="language-card">
          <h3>{{code}}</h3>
          {{progressBar progress}}
          <p class="progress-text">{{progress}}% Complete</p>
          <ul class="stats">
            <li>✅ Done: {{done}}</li>
            <li>⚠️ Outdated: {{outdated}}</li>
            <li>❌ Missing: {{missing}}</li>
          </ul>
        </div>
        {{/each}}
      </div>
    </section>

    <section class="files">
      <h2>File Status</h2>
      {{#each languages}}
      <details>
        <summary>{{code}} ({{done}}/{{total}})</summary>
        <table>
          <thead>
            <tr>
              <th>File</th>
              <th>Status</th>
              <th>Last Modified</th>
            </tr>
          </thead>
          <tbody>
            {{#each files}}
            <tr>
              <td>{{path}}</td>
              <td>{{statusIcon status}} {{status}}</td>
              <td>{{formatDate lastModified}}</td>
            </tr>
            {{/each}}
          </tbody>
        </table>
      </details>
      {{/each}}
    </section>
  </main>

  <footer>
    <p>Powered by <a href="https://lunaria.libdoc.top/">LunariaJS</a></p>
  </footer>
</body>
</html>

API 设计分析

公开 API

LunariaJS 导出以下公开 API:

// src/index.ts

export { loadConfig } from './config';
export { calculateAllStatus, calculateProgress } from './status';
export { generateDashboard } from './dashboard/generator';
export { getFileLastModified, getFileHistory } from './git';
export { parseFile, getParser } from './parsers';

// 类型导出
export type { LunariaConfig, FilePattern, DashboardConfig } from './types/config';
export type { TranslationStatus, FileStatus } from './types/status';
export type { LocalizedContent, FileParser } from './parsers/types';

使用示例

import {
  loadConfig,
  calculateAllStatus,
  generateDashboard,
  calculateProgress,
} from '@lunariajs/core';

async function main() {
  // 1. 加载配置
  const config = await loadConfig('lunaria.config.json');

  // 2. 计算翻译状态
  const status = await calculateAllStatus(config);

  // 3. 生成仪表板
  await generateDashboard(status, config.dashboard?.outputDir || 'lunaria-dashboard');

  // 4. 输出进度报告
  for (const lang of Object.keys(status.languages)) {
    const progress = calculateProgress(status, lang);
    console.log(`${lang}: ${progress}% complete`);
  }
}

main().catch(console.error);

源码阅读指南

入口文件分析

src/index.ts 开始,这是包的入口点:

  1. 导入依赖:加载配置、状态计算、仪表板生成等模块
  2. 导出 API:公开供外部使用的函数和类型
  3. 版本信息:导出当前版本号

核心模块导航

推荐阅读顺序

  1. src/types/ - 理解数据结构
  2. src/config.ts - 配置加载逻辑
  3. src/git.ts - Git 操作封装
  4. src/status.ts - 状态计算核心
  5. src/dashboard/ - 仪表板生成

调试技巧

// 启用调试日志
process.env.LUNARIA_DEBUG = 'true';

// 使用 verbose 模式
npx lunaria build --verbose

扩展开发基础

自定义解析器

// 自定义 CSV 解析器
import { FileParser, LocalizedContent } from '@lunariajs/core/parsers/types';

export const CsvParser: FileParser = {
  extensions: ['.csv'],

  async parse(content: string, path: string): Promise<LocalizedContent> {
    const lines = content.split('\n');
    const entries: Record<string, string> = {};

    for (const line of lines.slice(1)) { // 跳过表头
      const [key, value] = line.split(',');
      if (key && value) {
        entries[key.trim()] = value.trim();
      }
    }

    return {
      path,
      lang: extractLangFromPath(path),
      entries,
      raw: content,
    };
  },

  stringify(data: Record<string, string>): string {
    const lines = ['key,value'];
    for (const [key, value] of Object.entries(data)) {
      lines.push(`${key},${value}`);
    }
    return lines.join('\n');
  },
};

自定义状态计算策略

// 自定义状态计算逻辑
export async function customStatusCalculator(
  sourcePath: string,
  translationPath: string,
  options: { threshold: number }
): Promise<FileStatus> {
  // 使用自定义逻辑判断状态
  const sourceContent = await fs.readFile(sourcePath, 'utf-8');
  const translationContent = await fs.readFile(translationPath, 'utf-8');

  // 比较内容相似度
  const similarity = calculateSimilarity(sourceContent, translationContent);

  if (similarity < options.threshold) {
    return { status: 'outdated', similarity };
  }

  return { status: 'done', similarity };
}

贡献代码指南

  1. Fork 仓库https://github.com/withastro/lunaria
  2. 创建分支git checkout -b feature/your-feature
  3. 编写代码:遵循 TypeScript 和 ESLint 规范
  4. 运行测试npm test
  5. 提交 PR:描述清楚变更内容

总结

本章深入剖析了 @lunariajs/core 的核心架构:

模块核心功能关键技术
配置系统加载、验证、合并配置Zod Schema 验证
文件解析器解析 JSON/YAML/Markdown统一接口设计
状态追踪计算翻译状态Git 时间戳对比
Git 客户端封装 Git 操作simple-git
仪表板生成生成静态 HTMLHandlebars 模板

关键要点

  • 使用 TypeScript 提供类型安全
  • 模块化设计,职责清晰
  • Zod 进行配置验证
  • Git 历史追踪翻译状态
  • 模板引擎生成仪表板

下一步

下一篇文章,我们将探讨 LunariaJS 的高级配置与自定义策略,学习如何:

  • 处理企业级场景的复杂配置
  • 实现多仓库本地化管理
  • 设计自定义本地化策略
  • 优化大型项目的性能

敬请期待!


💡 推荐阅读