ブログ

@lunariajs/coreを深掘り:LunariaJSの核心アーキテクチャを理解する

LunariaJSコアライブラリのアーキテクチャ設計と実装原理を深く解説。ソースレベルでファイル解析、状態追跡、設定処理などのコアモジュールを理解し、カスタム拡張と高度な使い方の基盤を構築します。

LibDoc Team 2026年3月6日 LunariaJS 連載 135 分で読める
#LunariaJS #ソースコード解析 #コアアーキテクチャ #@lunariajs/core #TypeScript

@lunariajs/coreを深掘り:LunariaJSの核心アーキテクチャを理解する

これまでの7つの入門記事で、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[];
}

設定読み込みメカニズム

設定読み込みプロセスは3つのフェーズで構成されます:

// 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を全体として1つのエントリー
        '_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. リポジトリをForkhttps://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
ダッシュボード生成静的HTML生成Handlebarsテンプレート

重要なポイント

  • TypeScriptによる型安全性
  • モジュラー設計、責任の明確な分離
  • Zodによる設定バリデーション
  • Git履歴による翻訳状態追跡
  • テンプレートエンジンによるダッシュボード生成

次のステップ

次の記事では、LunariaJSの高度な設定とカスタム戦略について解説し、以下を学びます:

  • エンタープライズレベルの複雑設定の処理
  • 複数リポジトリのローカライズ管理
  • カスタムローカライズ戦略の設計
  • 大規模プロジェクトのパフォーマンス最適化

お楽しみに!


💡 おすすめ読書