czon 0.8.3 → 0.8.5

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.
@@ -39,6 +39,8 @@ const path = __importStar(require("path"));
39
39
  const metadata_1 = require("../metadata");
40
40
  const paths_1 = require("../paths");
41
41
  const category_1 = require("../process/category");
42
+ const translateCategories_1 = require("../process/translateCategories");
43
+ const translateNavLinks_1 = require("../process/translateNavLinks");
42
44
  const enhanceMarkdownSource_1 = require("../process/enhanceMarkdownSource");
43
45
  const extractMetadataByAI_1 = require("../process/extractMetadataByAI");
44
46
  const processTranslations_1 = require("../process/processTranslations");
@@ -103,6 +105,10 @@ async function buildPipeline(options) {
103
105
  await (0, extractMetadataByAI_1.extractMetadataByAI)();
104
106
  // 提取分类信息
105
107
  await (0, category_1.processExtractCategory)();
108
+ // 翻译分类
109
+ await (0, translateCategories_1.processTranslateCategories)();
110
+ // 翻译 navLinks
111
+ await (0, translateNavLinks_1.processTranslateNavLinks)();
106
112
  // 存储母语文件,并进行内容增强预处理
107
113
  await (0, enhanceMarkdownSource_1.storeNativeFiles)();
108
114
  // 处理翻译
@@ -0,0 +1,84 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.processTranslateCategories = void 0;
4
+ const metadata_1 = require("../metadata");
5
+ const openai_1 = require("../services/openai");
6
+ const processTranslateCategories = async () => {
7
+ const langs = metadata_1.MetaData.options.langs || [];
8
+ if (langs.length === 0) {
9
+ console.info('ℹ️ No target languages configured, skipping category translation.');
10
+ return;
11
+ }
12
+ if (!metadata_1.MetaData.categoryTranslations) {
13
+ metadata_1.MetaData.categoryTranslations = {};
14
+ }
15
+ const allCategories = [
16
+ ...new Set(metadata_1.MetaData.files.map(f => f.category).filter(Boolean)),
17
+ ];
18
+ if (allCategories.length === 0) {
19
+ console.info('ℹ️ No categories found, skipping category translation.');
20
+ return;
21
+ }
22
+ const categoriesNeedingTranslation = allCategories.filter(cat => {
23
+ const translations = metadata_1.MetaData.categoryTranslations[cat];
24
+ return !translations || langs.some(lang => !translations[lang]);
25
+ });
26
+ if (categoriesNeedingTranslation.length === 0) {
27
+ console.info('ℹ️ All categories already have translations, skipping.');
28
+ return;
29
+ }
30
+ console.info(`🌐 Translating ${categoriesNeedingTranslation.length} categories to ${langs.length} languages...`);
31
+ const response = await (0, openai_1.completeMessages)([
32
+ {
33
+ role: 'system',
34
+ content: [
35
+ '你是一个专业的翻译助手。',
36
+ '请将给定的分类名称翻译成指定的目标语言。',
37
+ '翻译应该自然、准确,符合目标语言的表达习惯。',
38
+ '分类名称通常是简短的词组,翻译时保持简洁。',
39
+ '',
40
+ '请以 JSON 格式返回,格式如下:',
41
+ '{',
42
+ ' "translations": {',
43
+ ' "CategoryKey": { "zh-Hans": "中文翻译", "en-US": "English" }',
44
+ ' }',
45
+ '}',
46
+ ].join('\n'),
47
+ },
48
+ {
49
+ role: 'user',
50
+ content: [
51
+ `需要翻译的分类: ${JSON.stringify(categoriesNeedingTranslation)}`,
52
+ `目标语言: ${JSON.stringify(langs)}`,
53
+ ].join('\n'),
54
+ },
55
+ ], { response_format: { type: 'json_object' }, task_id: 'translate-categories' });
56
+ const json = response.choices[0].message.content;
57
+ const parsed = JSON.parse(json);
58
+ if (parsed.translations) {
59
+ for (const [category, translations] of Object.entries(parsed.translations)) {
60
+ if (!metadata_1.MetaData.categoryTranslations[category]) {
61
+ metadata_1.MetaData.categoryTranslations[category] = {};
62
+ }
63
+ for (const [lang, translation] of Object.entries(translations)) {
64
+ metadata_1.MetaData.categoryTranslations[category][lang] = translation;
65
+ }
66
+ }
67
+ }
68
+ const stillMissingTranslation = allCategories.filter(cat => {
69
+ const translations = metadata_1.MetaData.categoryTranslations[cat];
70
+ return !translations || langs.some(lang => !translations[lang]);
71
+ });
72
+ if (stillMissingTranslation.length > 0) {
73
+ const details = stillMissingTranslation.map(cat => {
74
+ const missing = langs.filter(lang => !metadata_1.MetaData.categoryTranslations[cat]?.[lang]);
75
+ return ` - "${cat}": 缺少 ${missing.join(', ')}`;
76
+ });
77
+ console.error(`❌ 以下分类缺少翻译:\n${details.join('\n')}`);
78
+ console.error('这是一个可重试的错误,请重新运行命令。');
79
+ process.exit(1);
80
+ }
81
+ console.info('✅ Category translations completed.');
82
+ };
83
+ exports.processTranslateCategories = processTranslateCategories;
84
+ //# sourceMappingURL=translateCategories.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
@@ -6,6 +6,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.IndexPage = void 0;
7
7
  const react_1 = __importDefault(require("react"));
8
8
  const sortBy_1 = require("../utils/sortBy");
9
+ const getCategoryDisplayName_1 = require("./utils/getCategoryDisplayName");
9
10
  const Analytics_1 = require("./components/Analytics");
10
11
  const ContentMeta_1 = require("./components/ContentMeta");
11
12
  const CZONFooter_1 = require("./components/CZONFooter");
@@ -54,7 +55,9 @@ const IndexPage = props => {
54
55
  react_1.default.createElement("div", null,
55
56
  react_1.default.createElement("span", { className: "font-bold" }),
56
57
  react_1.default.createElement("div", { className: "mb-6 gap-6 flex flex-wrap" }, allCategories.map(category => {
57
- const title = category || 'All';
58
+ const title = category
59
+ ? (0, getCategoryDisplayName_1.getCategoryDisplayName)(props.ctx.site, category, props.lang)
60
+ : 'All';
58
61
  const link = category ? `categories_${category}.html` : 'index.html';
59
62
  const isActive = category === props.category;
60
63
  const articlesCount = category
@@ -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 }))));
@@ -5,6 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.ContentMeta = void 0;
7
7
  const react_1 = __importDefault(require("react"));
8
+ const getCategoryDisplayName_1 = require("../utils/getCategoryDisplayName");
8
9
  const TagList_1 = require("./TagList");
9
10
  const ContentMeta = props => {
10
11
  const content = props.ctx.contents.find(c => c.file === props.file && c.lang === props.lang);
@@ -17,7 +18,7 @@ const ContentMeta = props => {
17
18
  return (react_1.default.createElement("header", { className: "content-header mb-4 pb-2 border-b" },
18
19
  react_1.default.createElement("h2", { className: "text-2xl font-bold mb-2" },
19
20
  react_1.default.createElement("a", { href: `${props.file.metadata?.slug}.html` }, title)),
20
- react_1.default.createElement("p", { className: "font-semibold" }, category),
21
+ react_1.default.createElement("p", { className: "font-semibold" }, (0, getCategoryDisplayName_1.getCategoryDisplayName)(props.ctx.site, category, props.lang)),
21
22
  react_1.default.createElement("blockquote", null, summary),
22
23
  react_1.default.createElement("div", null,
23
24
  "\uD83D\uDCC5 ",
@@ -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
@@ -5,6 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.Navigator = void 0;
7
7
  const react_1 = __importDefault(require("react"));
8
+ const getCategoryDisplayName_1 = require("../utils/getCategoryDisplayName");
8
9
  const sortBy_1 = require("../../utils/sortBy");
9
10
  const Navigator = props => {
10
11
  const categories = [...new Set(props.ctx.site.files.map(f => f.category))];
@@ -19,7 +20,7 @@ const Navigator = props => {
19
20
  if (filesInCategory.length === 0)
20
21
  return null;
21
22
  return (react_1.default.createElement("div", { key: categoryKey },
22
- react_1.default.createElement("li", { className: "nav-item font-bold", key: categoryKey }, category || '--'),
23
+ react_1.default.createElement("li", { className: "nav-item font-bold", key: categoryKey }, (0, getCategoryDisplayName_1.getCategoryDisplayName)(props.ctx.site, category, props.lang) || '--'),
23
24
  filesInCategory.map(file => {
24
25
  const link = file.metadata.slug + '.html';
25
26
  const isActive = props.file === file;
@@ -0,0 +1,9 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getCategoryDisplayName = getCategoryDisplayName;
4
+ function getCategoryDisplayName(site, category, lang) {
5
+ if (!category)
6
+ return '';
7
+ return site.categoryTranslations?.[category]?.[lang] || category;
8
+ }
9
+ //# sourceMappingURL=getCategoryDisplayName.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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "czon",
3
- "version": "0.8.3",
3
+ "version": "0.8.5",
4
4
  "description": "CZON - AI enhanced Markdown content engine",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",