czon 0.8.4 → 0.8.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -21,7 +21,7 @@ async function extractMetadataFromMarkdown(filePath, content) {
21
21
  'summary: 文档中型摘要(用一段话总结文章,包含关键论点和结论,控制在 300 字以内),用于 邮件推送内容,newsletter 介绍',
22
22
  'inferred_date: 文档中隐含的创建日期(如果有的话,格式:YYYY-MM-DD,没有就留空字符串)',
23
23
  'inferred_lang: 文档使用的语言代码(例如:zh-Hans 表示简体中文,en-US 表示美式英语)',
24
- 'key_points: 文章的关键要点列表(5-10 个要点,简洁明了)',
24
+ 'key_points: 文章的关键要点列表(最重要的 3-5 个要点,简洁明了)',
25
25
  'audience: 目标读者描述(简短描述,50 字以内)',
26
26
  'short_summary: 文档的超短摘要(用 2-3 句话概括文章主要内容,突出核心观点),用于文章列表页摘要,RSS feed 描述',
27
27
  ...(hasExistingSlug
@@ -40,6 +40,8 @@ const metadata_1 = require("../metadata");
40
40
  const paths_1 = require("../paths");
41
41
  const category_1 = require("../process/category");
42
42
  const translateCategories_1 = require("../process/translateCategories");
43
+ const translateMetadata_1 = require("../process/translateMetadata");
44
+ const translateNavLinks_1 = require("../process/translateNavLinks");
43
45
  const enhanceMarkdownSource_1 = require("../process/enhanceMarkdownSource");
44
46
  const extractMetadataByAI_1 = require("../process/extractMetadataByAI");
45
47
  const processTranslations_1 = require("../process/processTranslations");
@@ -102,10 +104,14 @@ async function buildPipeline(options) {
102
104
  }
103
105
  // 运行 AI 元数据提取
104
106
  await (0, extractMetadataByAI_1.extractMetadataByAI)();
107
+ // 翻译 AI 提取的 metadata JSON
108
+ await (0, translateMetadata_1.processTranslateMetadata)();
105
109
  // 提取分类信息
106
110
  await (0, category_1.processExtractCategory)();
107
111
  // 翻译分类
108
112
  await (0, translateCategories_1.processTranslateCategories)();
113
+ // 翻译 navLinks
114
+ await (0, translateNavLinks_1.processTranslateNavLinks)();
109
115
  // 存储母语文件,并进行内容增强预处理
110
116
  await (0, enhanceMarkdownSource_1.storeNativeFiles)();
111
117
  // 处理翻译
@@ -8,10 +8,10 @@ const promises_1 = require("fs/promises");
8
8
  const path_1 = __importDefault(require("path"));
9
9
  const metadata_1 = require("../metadata");
10
10
  const paths_1 = require("../paths");
11
- const frontmatter_1 = require("../utils/frontmatter");
12
11
  const writeFile_1 = require("../utils/writeFile");
13
12
  /**
14
13
  * 存储母语文件到 .czon/src
14
+ * 直接复制原始文件,metadata 从 meta.json 获取
15
15
  */
16
16
  async function storeNativeFiles() {
17
17
  const { files } = metadata_1.MetaData;
@@ -25,16 +25,7 @@ async function storeNativeFiles() {
25
25
  throw new Error(`Missing inferred language`);
26
26
  const filePath = path_1.default.join(paths_1.CZON_SRC_DIR, file.metadata.inferred_lang, file.path);
27
27
  const originalContent = await (0, promises_1.readFile)(path_1.default.join(paths_1.INPUT_DIR, file.path), 'utf-8');
28
- // 增强 YAML Frontmatter
29
- const enhancedContent = (0, frontmatter_1.updateFrontmatter)(originalContent, {
30
- title: file.metadata.title,
31
- summary: file.metadata.summary,
32
- tags: file.metadata.tags,
33
- date: file.metadata.inferred_date,
34
- });
35
- // 进行内链接替换, 将相对链接替换为基于 czon://hash 的链接
36
- // const replacedContent = replaceInnerLinks(file, enhancedContent);
37
- await (0, writeFile_1.writeFile)(filePath, enhancedContent);
28
+ await (0, writeFile_1.writeFile)(filePath, originalContent);
38
29
  }
39
30
  catch (error) {
40
31
  console.warn(`⚠️ Failed to store native file ${file.path}:`, error);
@@ -102,12 +102,11 @@ const spiderStaticSiteGenerator = async () => {
102
102
  continue;
103
103
  for (const lang of metadata_1.MetaData.options.langs || []) {
104
104
  const markdown = await fs.readFile(path.join(paths_1.CZON_SRC_DIR, lang, file.path), 'utf-8');
105
- const { frontmatter, body } = (0, frontmatter_1.parseFrontmatter)(markdown);
105
+ const body = (0, frontmatter_1.stripFrontmatter)(markdown);
106
106
  const article = {
107
107
  lang,
108
108
  file,
109
109
  body: '',
110
- frontmatter,
111
110
  headings: [],
112
111
  };
113
112
  (0, convertMarkdownToHtml_1.convertMarkdownToHtml)(article, file.path, lang, body);
@@ -0,0 +1,172 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.processTranslateMetadata = processTranslateMetadata;
4
+ const metadata_1 = require("../metadata");
5
+ const openai_1 = require("../services/openai");
6
+ const languages_1 = require("../languages");
7
+ const sha256_1 = require("../utils/sha256");
8
+ /**
9
+ * 计算 metadata 中需要翻译字段的哈希值
10
+ * 用于增量检测
11
+ */
12
+ function computeTranslatableFieldsHash(metadata) {
13
+ const translatableFields = {
14
+ title: metadata.title,
15
+ description: metadata.description,
16
+ summary: metadata.summary,
17
+ short_summary: metadata.short_summary,
18
+ key_points: metadata.key_points,
19
+ audience: metadata.audience,
20
+ tags: metadata.tags,
21
+ };
22
+ return (0, sha256_1.sha256)(JSON.stringify(translatableFields));
23
+ }
24
+ /**
25
+ * 检查单个文件的单个语言是否需要翻译
26
+ */
27
+ function needsTranslationForLang(file, lang) {
28
+ if (!file.metadata)
29
+ return false;
30
+ // 没有该语言的翻译 -> 需要翻译
31
+ if (!file.metadataTranslations?.[lang])
32
+ return true;
33
+ // hash 变化 -> 源 metadata 已更新,需要重新翻译
34
+ const currentHash = computeTranslatableFieldsHash(file.metadata);
35
+ return file.metadataTranslationHash !== currentHash;
36
+ }
37
+ /**
38
+ * 翻译单个文件的 metadata 到单个目标语言
39
+ */
40
+ async function translateFileMetadataToLang(file, targetLang) {
41
+ const { metadata } = file;
42
+ if (!metadata)
43
+ return;
44
+ // 提取需要翻译的字段
45
+ const sourceMetadata = {
46
+ title: metadata.title,
47
+ description: metadata.description,
48
+ summary: metadata.summary,
49
+ short_summary: metadata.short_summary,
50
+ key_points: metadata.key_points,
51
+ audience: metadata.audience,
52
+ tags: metadata.tags,
53
+ };
54
+ const sourceLangName = languages_1.LANGUAGE_NAMES[metadata.inferred_lang] || metadata.inferred_lang;
55
+ const targetLangName = languages_1.LANGUAGE_NAMES[targetLang] || targetLang;
56
+ const response = await (0, openai_1.completeMessages)([
57
+ {
58
+ role: 'system',
59
+ content: [
60
+ '你是专业的多语言翻译助手,擅长准确翻译技术文档的元数据。',
61
+ '',
62
+ '任务:将给定的 metadata JSON 翻译成目标语言。',
63
+ '',
64
+ '要求:',
65
+ '1. 保持 JSON 结构完全不变,只翻译文本内容',
66
+ '2. title 翻译要简洁明了,不超过 30 个字',
67
+ '3. description 和 summary 翻译要准确传达原意',
68
+ '4. tags 翻译要符合目标语言的常用术语',
69
+ '5. key_points 数组中的每个要点都需要翻译',
70
+ '6. 技术术语保持准确,必要时可保留原文',
71
+ '',
72
+ '直接返回翻译后的 JSON 对象,格式如下:',
73
+ '{',
74
+ ' "title": "...",',
75
+ ' "description": "...",',
76
+ ' "summary": "...",',
77
+ ' "short_summary": "...",',
78
+ ' "key_points": ["...", "..."],',
79
+ ' "audience": "...",',
80
+ ' "tags": ["...", "..."]',
81
+ '}',
82
+ ].join('\n'),
83
+ },
84
+ {
85
+ role: 'user',
86
+ content: [
87
+ `源语言: ${metadata.inferred_lang} (${sourceLangName})`,
88
+ `目标语言: ${targetLang} (${targetLangName})`,
89
+ '',
90
+ '待翻译的 metadata:',
91
+ JSON.stringify(sourceMetadata, null, 2),
92
+ ].join('\n'),
93
+ },
94
+ ], {
95
+ response_format: { type: 'json_object' },
96
+ task_id: `translate-metadata:${file.path}:${targetLang}`,
97
+ });
98
+ const translated = JSON.parse(response.choices[0].message.content);
99
+ // 初始化存储结构
100
+ if (!file.metadataTranslations) {
101
+ file.metadataTranslations = {};
102
+ }
103
+ // 验证并存储翻译结果
104
+ if (translated && translated.title) {
105
+ file.metadataTranslations[targetLang] = {
106
+ title: translated.title?.trim() || '',
107
+ description: translated.description?.trim() || '',
108
+ summary: translated.summary?.trim() || '',
109
+ short_summary: translated.short_summary?.trim() || '',
110
+ key_points: Array.isArray(translated.key_points)
111
+ ? translated.key_points.map((p) => p?.trim()).filter(Boolean)
112
+ : [],
113
+ audience: translated.audience?.trim() || '',
114
+ tags: Array.isArray(translated.tags)
115
+ ? translated.tags.map((t) => t?.trim()).filter(Boolean)
116
+ : [],
117
+ };
118
+ }
119
+ }
120
+ /**
121
+ * 处理所有文件的 metadata 翻译
122
+ * 采用逐文件逐语言的翻译策略,避免输出 JSON 过长被截断
123
+ */
124
+ async function processTranslateMetadata() {
125
+ const langs = metadata_1.MetaData.options.langs || [];
126
+ if (langs.length === 0) {
127
+ console.info('ℹ️ No target languages configured, skipping metadata translation.');
128
+ return;
129
+ }
130
+ // 收集所有需要翻译的任务 (file, lang) 组合
131
+ const tasks = [];
132
+ for (const file of metadata_1.MetaData.files) {
133
+ if (!file.path.endsWith('.md') || !file.metadata)
134
+ continue;
135
+ for (const lang of langs) {
136
+ if (needsTranslationForLang(file, lang)) {
137
+ tasks.push({ file, lang });
138
+ }
139
+ }
140
+ }
141
+ if (tasks.length === 0) {
142
+ console.info('ℹ️ All metadata already translated, skipping.');
143
+ return;
144
+ }
145
+ const fileCount = new Set(tasks.map(t => t.file.path)).size;
146
+ console.info(`🌐 Translating metadata: ${tasks.length} tasks (${fileCount} files × up to ${langs.length} languages)...`);
147
+ // 并行执行所有翻译任务
148
+ const results = await Promise.allSettled(tasks.map(async ({ file, lang }) => {
149
+ await translateFileMetadataToLang(file, lang);
150
+ console.info(`✅ Translated metadata: ${file.path} -> ${lang}`);
151
+ }));
152
+ // 统计结果
153
+ const succeeded = results.filter(r => r.status === 'fulfilled').length;
154
+ const failed = results.filter(r => r.status === 'rejected').length;
155
+ if (failed > 0) {
156
+ console.warn(`⚠️ ${failed} translation tasks failed:`);
157
+ results.forEach((r, i) => {
158
+ if (r.status === 'rejected') {
159
+ console.error(` ❌ ${tasks[i].file.path} -> ${tasks[i].lang}:`, r.reason?.message || r.reason);
160
+ }
161
+ });
162
+ }
163
+ // 更新所有成功翻译文件的 hash
164
+ const translatedFiles = new Set(tasks.filter((_, i) => results[i].status === 'fulfilled').map(t => t.file));
165
+ for (const file of translatedFiles) {
166
+ if (file.metadata) {
167
+ file.metadataTranslationHash = computeTranslatableFieldsHash(file.metadata);
168
+ }
169
+ }
170
+ console.info(`✅ Metadata translation completed: ${succeeded}/${tasks.length} succeeded`);
171
+ }
172
+ //# sourceMappingURL=translateMetadata.js.map
@@ -0,0 +1,79 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.processTranslateNavLinks = void 0;
4
+ const metadata_1 = require("../metadata");
5
+ const openai_1 = require("../services/openai");
6
+ const processTranslateNavLinks = async () => {
7
+ const langs = metadata_1.MetaData.options.langs || [];
8
+ const navLinks = metadata_1.MetaData.options.site?.navLinks || [];
9
+ if (langs.length === 0 || navLinks.length === 0) {
10
+ console.info('ℹ️ No languages or navLinks configured, skipping navLink translation.');
11
+ return;
12
+ }
13
+ if (!metadata_1.MetaData.navLinkTranslations) {
14
+ metadata_1.MetaData.navLinkTranslations = {};
15
+ }
16
+ const allTitles = navLinks.map(link => link.title);
17
+ const titlesNeedingTranslation = allTitles.filter(title => {
18
+ const translations = metadata_1.MetaData.navLinkTranslations[title];
19
+ return !translations || langs.some(lang => !translations[lang]);
20
+ });
21
+ if (titlesNeedingTranslation.length === 0) {
22
+ console.info('ℹ️ All navLink titles already have translations, skipping.');
23
+ return;
24
+ }
25
+ console.info(`🌐 Translating ${titlesNeedingTranslation.length} navLink titles...`);
26
+ const response = await (0, openai_1.completeMessages)([
27
+ {
28
+ role: 'system',
29
+ content: [
30
+ '你是一个专业的翻译助手。',
31
+ '请将给定的导航链接标题翻译成指定的目标语言。',
32
+ '翻译应该自然、准确,符合目标语言的表达习惯。',
33
+ '导航链接标题通常是简短的词语,翻译时保持简洁。',
34
+ '',
35
+ '请以 JSON 格式返回,格式如下:',
36
+ '{',
37
+ ' "translations": {',
38
+ ' "原标题": { "zh-Hans": "中文翻译", "en-US": "English" }',
39
+ ' }',
40
+ '}',
41
+ ].join('\n'),
42
+ },
43
+ {
44
+ role: 'user',
45
+ content: [
46
+ `需要翻译的导航链接标题: ${JSON.stringify(titlesNeedingTranslation)}`,
47
+ `目标语言: ${JSON.stringify(langs)}`,
48
+ ].join('\n'),
49
+ },
50
+ ], { response_format: { type: 'json_object' }, task_id: 'translate-nav-links' });
51
+ const json = response.choices[0].message.content;
52
+ const parsed = JSON.parse(json);
53
+ if (parsed.translations) {
54
+ for (const [title, translations] of Object.entries(parsed.translations)) {
55
+ if (!metadata_1.MetaData.navLinkTranslations[title]) {
56
+ metadata_1.MetaData.navLinkTranslations[title] = {};
57
+ }
58
+ for (const [lang, translation] of Object.entries(translations)) {
59
+ metadata_1.MetaData.navLinkTranslations[title][lang] = translation;
60
+ }
61
+ }
62
+ }
63
+ const stillMissingTranslation = allTitles.filter(title => {
64
+ const translations = metadata_1.MetaData.navLinkTranslations[title];
65
+ return !translations || langs.some(lang => !translations[lang]);
66
+ });
67
+ if (stillMissingTranslation.length > 0) {
68
+ const details = stillMissingTranslation.map(title => {
69
+ const missing = langs.filter(lang => !metadata_1.MetaData.navLinkTranslations[title]?.[lang]);
70
+ return ` - "${title}": 缺少 ${missing.join(', ')}`;
71
+ });
72
+ console.error(`❌ 以下 navLink 标题缺少翻译:\n${details.join('\n')}`);
73
+ console.error('这是一个可重试的错误,请重新运行命令。');
74
+ process.exit(1);
75
+ }
76
+ console.info('✅ NavLink translations completed.');
77
+ };
78
+ exports.processTranslateNavLinks = processTranslateNavLinks;
79
+ //# sourceMappingURL=translateNavLinks.js.map
@@ -15,12 +15,13 @@ const Navigator_1 = require("./components/Navigator");
15
15
  const PageLayout_1 = require("./layouts/PageLayout");
16
16
  const resourceMap_1 = require("./resourceMap");
17
17
  const style_1 = require("./style");
18
+ const getLocalizedMetadata_1 = require("./utils/getLocalizedMetadata");
18
19
  const ContentPage = props => {
19
- const frontmatter = props.content.frontmatter || {};
20
- const title = frontmatter.title;
21
- const summary = frontmatter.summary;
22
- const date = frontmatter.date || '--';
23
- const tags = frontmatter.tags || [];
20
+ const metadata = (0, getLocalizedMetadata_1.getLocalizedMetadata)(props.file, props.lang);
21
+ const title = metadata?.title || '(no title)';
22
+ const summary = metadata?.summary || '';
23
+ const date = props.file.metadata?.inferred_date || '--';
24
+ const tags = metadata?.tags || [];
24
25
  const category = props.file.category;
25
26
  const relatedContents = props.ctx.site.files.filter(f => f.category === category && f.path !== props.file.path);
26
27
  // 查找指向当前文章的其他文章
@@ -34,7 +35,10 @@ const ContentPage = props => {
34
35
  react_1.default.createElement("meta", { name: "viewport", content: "width=device-width, initial-scale=1.0" }),
35
36
  react_1.default.createElement("title", null, title),
36
37
  react_1.default.createElement("link", { rel: "icon", href: faviconUrl, type: "image/x-icon" }),
37
- react_1.default.createElement("meta", { name: "description", content: `tags: ${tags.join(', ')}` }),
38
+ // 使用 short_summary 作为 description,有利于 SEO 和社交分享预览
39
+ metadata?.short_summary && (react_1.default.createElement("meta", { name: "description", content: metadata?.short_summary.slice(0, 150) })),
40
+ // Keywords meta tag for SEO (using tags, if available)
41
+ tags.length > 0 && react_1.default.createElement("meta", { name: "keywords", content: tags.join(', ') }),
38
42
  react_1.default.createElement(Analytics_1.Analytics, { ctx: props.ctx }),
39
43
  react_1.default.createElement("script", { src: (0, resourceMap_1.getResourceUrlFrom)(props.ctx.path, 'tailwindcss.js') }),
40
44
  react_1.default.createElement("style", null, style_1.style),
@@ -69,16 +73,16 @@ const ContentPage = props => {
69
73
  relatedContents.length > 0 && (react_1.default.createElement(react_1.default.Fragment, null,
70
74
  react_1.default.createElement("h2", null, "See Also"),
71
75
  react_1.default.createElement("ul", null, relatedContents.map(f => {
72
- const theContent = props.ctx.contents.find(c => c.lang === props.lang && c.file === f);
76
+ const theMetadata = (0, getLocalizedMetadata_1.getLocalizedMetadata)(f, props.lang);
73
77
  return (react_1.default.createElement("li", { key: f.path },
74
- react_1.default.createElement("a", { href: `${f.metadata?.slug}.html` }, theContent?.frontmatter.title)));
78
+ react_1.default.createElement("a", { href: `${f.metadata?.slug}.html` }, theMetadata?.title || '(no title)')));
75
79
  })))),
76
80
  referencedFiles.length > 0 && (react_1.default.createElement(react_1.default.Fragment, null,
77
81
  react_1.default.createElement("h2", null, "Referenced By"),
78
82
  react_1.default.createElement("ul", null, referencedFiles.map(f => {
79
- const theContent = props.ctx.contents.find(c => c.lang === props.lang && c.file === f);
83
+ const theMetadata = (0, getLocalizedMetadata_1.getLocalizedMetadata)(f, props.lang);
80
84
  return (react_1.default.createElement("li", { key: f.path },
81
- react_1.default.createElement("a", { href: `${f.metadata?.slug}.html` }, theContent?.frontmatter.title)));
85
+ react_1.default.createElement("a", { href: `${f.metadata?.slug}.html` }, theMetadata?.title || '(no title)')));
82
86
  }))))),
83
87
  react_1.default.createElement("footer", { className: "footer" },
84
88
  react_1.default.createElement(LanguageSwitcher_1.LanguageSwitcher, { ctx: props.ctx, lang: props.lang, file: props.file }),
@@ -14,10 +14,10 @@ const CZONHeader = props => {
14
14
  const home = props.ctx.site.options.site?.home ?? 'index.html';
15
15
  return (react_1.default.createElement("header", { className: "czon-header py-4 border-b flex justify-between items-center px-6" },
16
16
  react_1.default.createElement("div", { className: "flex items-center gap-4" },
17
- hasNavLinks && react_1.default.createElement(NavLinks_1.NavLinksMobile, { navLinks: navLinks }),
17
+ hasNavLinks && (react_1.default.createElement(NavLinks_1.NavLinksMobile, { navLinks: navLinks, site: props.ctx.site, lang: props.lang })),
18
18
  react_1.default.createElement("h1", { className: "text-2xl font-bold" },
19
19
  react_1.default.createElement("a", { href: home }, props.ctx.site.options.site?.title ?? 'CZON')),
20
- hasNavLinks && react_1.default.createElement(NavLinks_1.NavLinksDesktop, { navLinks: navLinks })),
20
+ hasNavLinks && (react_1.default.createElement(NavLinks_1.NavLinksDesktop, { navLinks: navLinks, site: props.ctx.site, lang: props.lang }))),
21
21
  react_1.default.createElement("div", { className: "flex items-center gap-4" },
22
22
  react_1.default.createElement(DarkModeSwitch_1.DarkModeSwitch, null),
23
23
  props.lang && react_1.default.createElement(LanguageSwitch_1.LanguageSwitch, { ctx: props.ctx, lang: props.lang, file: props.file }))));
@@ -6,21 +6,29 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.ContentMeta = void 0;
7
7
  const react_1 = __importDefault(require("react"));
8
8
  const getCategoryDisplayName_1 = require("../utils/getCategoryDisplayName");
9
+ const getLocalizedMetadata_1 = require("../utils/getLocalizedMetadata");
9
10
  const TagList_1 = require("./TagList");
10
11
  const ContentMeta = props => {
11
- const content = props.ctx.contents.find(c => c.file === props.file && c.lang === props.lang);
12
- const frontmatter = content?.frontmatter || {};
13
- const title = frontmatter.title;
14
- const summary = frontmatter.summary;
15
- const date = frontmatter.date || '--';
16
- const tags = frontmatter.tags || [];
12
+ const metadata = (0, getLocalizedMetadata_1.getLocalizedMetadata)(props.file, props.lang);
13
+ const title = metadata?.title || '(no title)';
14
+ const summary = metadata?.summary || '';
15
+ const audience = metadata?.audience || '';
16
+ const keyPoints = metadata?.key_points || [];
17
+ const date = props.file.metadata?.inferred_date || '--';
18
+ const tags = metadata?.tags || [];
17
19
  const category = props.file.category;
18
20
  return (react_1.default.createElement("header", { className: "content-header mb-4 pb-2 border-b" },
19
21
  react_1.default.createElement("h2", { className: "text-2xl font-bold mb-2" },
20
22
  react_1.default.createElement("a", { href: `${props.file.metadata?.slug}.html` }, title)),
21
- react_1.default.createElement("p", { className: "font-semibold" }, (0, getCategoryDisplayName_1.getCategoryDisplayName)(props.ctx.site, category, props.lang)),
23
+ react_1.default.createElement("p", { className: "font-semibold mb-2" }, (0, getCategoryDisplayName_1.getCategoryDisplayName)(props.ctx.site, category, props.lang)),
24
+ audience && react_1.default.createElement("div", null,
25
+ "\uD83D\uDC64 ",
26
+ audience),
22
27
  react_1.default.createElement("blockquote", null, summary),
23
- react_1.default.createElement("div", null,
28
+ keyPoints.length > 0 && (react_1.default.createElement("ul", { className: "key-points list-inside my-2" }, keyPoints.slice(0, 5).map((point, index) => (react_1.default.createElement("li", { key: index },
29
+ "\u2728 ",
30
+ point))))),
31
+ date && date !== '--' && react_1.default.createElement("div", null,
24
32
  "\uD83D\uDCC5 ",
25
33
  date),
26
34
  react_1.default.createElement("div", { className: "tags" },
@@ -5,6 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.NavLinksDesktop = exports.NavLinksMobile = void 0;
7
7
  const react_1 = __importDefault(require("react"));
8
+ const getNavLinkDisplayTitle_1 = require("../utils/getNavLinkDisplayTitle");
8
9
  const mobileStyle = `
9
10
  /* Mobile hamburger menu styles */
10
11
  .nav-links-mobile {
@@ -233,7 +234,7 @@ const desktopStyle = `
233
234
  * 移动端汉堡菜单导航组件
234
235
  * 仅在移动端显示(< 768px)
235
236
  */
236
- const NavLinksMobile = ({ navLinks }) => {
237
+ const NavLinksMobile = ({ navLinks, site, lang }) => {
237
238
  if (!navLinks || navLinks.length === 0) {
238
239
  return null;
239
240
  }
@@ -246,7 +247,7 @@ const NavLinksMobile = ({ navLinks }) => {
246
247
  react_1.default.createElement("span", null),
247
248
  react_1.default.createElement("span", null))),
248
249
  react_1.default.createElement("div", { className: "nav-links-dropdown", role: "menu", "aria-label": "Navigation links" },
249
- react_1.default.createElement("div", { className: "nav-links-dropdown-list" }, navLinks.map((link, index) => (react_1.default.createElement("a", { key: index, href: link.href, className: "nav-link-mobile-item", role: "menuitem" }, link.title)))))));
250
+ react_1.default.createElement("div", { className: "nav-links-dropdown-list" }, navLinks.map((link, index) => (react_1.default.createElement("a", { key: index, href: link.href, className: "nav-link-mobile-item", role: "menuitem" }, (0, getNavLinkDisplayTitle_1.getNavLinkDisplayTitle)(site, link.title, lang || ''))))))));
250
251
  };
251
252
  exports.NavLinksMobile = NavLinksMobile;
252
253
  /**
@@ -259,7 +260,7 @@ const MAX_VISIBLE_LINKS = 5;
259
260
  * 桌面端导航组件
260
261
  * 仅在桌面端显示(>= 768px),最大宽度 40vw
261
262
  */
262
- const NavLinksDesktop = ({ navLinks }) => {
263
+ const NavLinksDesktop = ({ navLinks, site, lang }) => {
263
264
  if (!navLinks || navLinks.length === 0) {
264
265
  return null;
265
266
  }
@@ -268,7 +269,7 @@ const NavLinksDesktop = ({ navLinks }) => {
268
269
  const hasOverflow = overflowLinks.length > 0;
269
270
  return (react_1.default.createElement("nav", { className: "nav-links-desktop", "aria-label": "Main navigation" },
270
271
  react_1.default.createElement("style", null, desktopStyle),
271
- react_1.default.createElement("div", { className: "nav-links-desktop-list" }, visibleLinks.map((link, index) => (react_1.default.createElement("a", { key: index, href: link.href, className: "nav-link-item" }, link.title)))),
272
+ react_1.default.createElement("div", { className: "nav-links-desktop-list" }, visibleLinks.map((link, index) => (react_1.default.createElement("a", { key: index, href: link.href, className: "nav-link-item" }, (0, getNavLinkDisplayTitle_1.getNavLinkDisplayTitle)(site, link.title, lang || ''))))),
272
273
  hasOverflow && (react_1.default.createElement("div", { className: "nav-links-more-container" },
273
274
  react_1.default.createElement("input", { id: "nav-links-more-toggle", type: "checkbox", className: "hidden", "aria-hidden": "true" }),
274
275
  react_1.default.createElement("label", { htmlFor: "nav-links-more-toggle", className: "nav-links-more-trigger", "aria-label": "More navigation links", "aria-haspopup": "true", "aria-expanded": "false" },
@@ -276,7 +277,7 @@ const NavLinksDesktop = ({ navLinks }) => {
276
277
  react_1.default.createElement("svg", { className: "nav-links-more-icon", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", "aria-hidden": "true" },
277
278
  react_1.default.createElement("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M19 9l-7 7-7-7" }))),
278
279
  react_1.default.createElement("div", { className: "nav-links-more-dropdown", role: "menu", "aria-label": "Additional navigation links" },
279
- react_1.default.createElement("div", { className: "nav-links-more-list" }, overflowLinks.map((link, index) => (react_1.default.createElement("a", { key: index, href: link.href, className: "nav-link-more-item", role: "menuitem" }, link.title)))))))));
280
+ react_1.default.createElement("div", { className: "nav-links-more-list" }, overflowLinks.map((link, index) => (react_1.default.createElement("a", { key: index, href: link.href, className: "nav-link-more-item", role: "menuitem" }, (0, getNavLinkDisplayTitle_1.getNavLinkDisplayTitle)(site, link.title, lang || ''))))))))));
280
281
  };
281
282
  exports.NavLinksDesktop = NavLinksDesktop;
282
283
  //# sourceMappingURL=NavLinks.js.map
@@ -6,6 +6,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.Navigator = void 0;
7
7
  const react_1 = __importDefault(require("react"));
8
8
  const getCategoryDisplayName_1 = require("../utils/getCategoryDisplayName");
9
+ const getLocalizedMetadata_1 = require("../utils/getLocalizedMetadata");
9
10
  const sortBy_1 = require("../../utils/sortBy");
10
11
  const Navigator = props => {
11
12
  const categories = [...new Set(props.ctx.site.files.map(f => f.category))];
@@ -24,8 +25,8 @@ const Navigator = props => {
24
25
  filesInCategory.map(file => {
25
26
  const link = file.metadata.slug + '.html';
26
27
  const isActive = props.file === file;
27
- const theContent = props.ctx.contents.find(c => c.lang === props.lang && c.file === file);
28
- const theTitle = theContent?.frontmatter?.title || file.metadata.title || '(no title)';
28
+ const theMetadata = (0, getLocalizedMetadata_1.getLocalizedMetadata)(file, props.lang);
29
+ const theTitle = theMetadata?.title || '(no title)';
29
30
  return (react_1.default.createElement("li", { className: "nav-item", key: file.path },
30
31
  react_1.default.createElement("a", { href: link, className: `nav-link ${isActive ? 'active' : ''}` },
31
32
  theTitle,
@@ -0,0 +1,20 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getLocalizedMetadata = getLocalizedMetadata;
4
+ /**
5
+ * 获取文件在指定语言下的 metadata
6
+ * - 如果是原语言,返回 file.metadata
7
+ * - 如果是翻译语言,返回 file.metadataTranslations[lang]
8
+ */
9
+ function getLocalizedMetadata(file, lang) {
10
+ const metadata = file.metadata;
11
+ if (!metadata)
12
+ return undefined;
13
+ // 如果是原语言,直接返回原始 metadata
14
+ if (lang === metadata.inferred_lang) {
15
+ return metadata;
16
+ }
17
+ // 否则返回翻译版本
18
+ return file.metadataTranslations?.[lang];
19
+ }
20
+ //# sourceMappingURL=getLocalizedMetadata.js.map
@@ -0,0 +1,7 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getNavLinkDisplayTitle = getNavLinkDisplayTitle;
4
+ function getNavLinkDisplayTitle(site, title, lang) {
5
+ return site.navLinkTranslations?.[title]?.[lang] || title;
6
+ }
7
+ //# sourceMappingURL=getNavLinkDisplayTitle.js.map
@@ -1,9 +1,9 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.updateFrontmatter = exports.parseFrontmatter = void 0;
3
+ exports.stripFrontmatter = exports.parseFrontmatter = void 0;
4
4
  const yaml_1 = require("yaml");
5
+ const frontmatterRegex = /^---\n([\s\S]*?)\n---/;
5
6
  const parseFrontmatter = (content) => {
6
- const frontmatterRegex = /^---\n([\s\S]*?)\n---/;
7
7
  const match = content.match(frontmatterRegex);
8
8
  if (match) {
9
9
  const frontmatterContent = match[1];
@@ -13,10 +13,15 @@ const parseFrontmatter = (content) => {
13
13
  return { frontmatter: {}, body: content };
14
14
  };
15
15
  exports.parseFrontmatter = parseFrontmatter;
16
- const updateFrontmatter = (content, newFrontmatter) => {
17
- const { body } = (0, exports.parseFrontmatter)(content);
18
- const frontmatterContent = `---\n${(0, yaml_1.stringify)(newFrontmatter, { defaultStringType: 'QUOTE_DOUBLE' })}---\n\n`;
19
- return frontmatterContent + body;
16
+ /**
17
+ * 移除 Markdown 内容中的 YAML FrontMatter,只返回正文
18
+ */
19
+ const stripFrontmatter = (content) => {
20
+ const match = content.match(frontmatterRegex);
21
+ if (match) {
22
+ return content.slice(match[0].length).trim();
23
+ }
24
+ return content;
20
25
  };
21
- exports.updateFrontmatter = updateFrontmatter;
26
+ exports.stripFrontmatter = stripFrontmatter;
22
27
  //# sourceMappingURL=frontmatter.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "czon",
3
- "version": "0.8.4",
3
+ "version": "0.8.6",
4
4
  "description": "CZON - AI enhanced Markdown content engine",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",