hexo-theme-gnix 13.0.0 → 14.0.0
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.
- package/include/hexo/generator/archive.js +14 -1
- package/include/hexo/generator/index.js +0 -5
- package/include/hexo/generator/page.js +18 -4
- package/include/hexo/generator/tag.js +1 -1
- package/include/hexo/helper.js +0 -4
- package/include/hexo/i18n.js +21 -136
- package/include/util/i18n.js +92 -106
- package/layout/archive.jsx +155 -78
- package/layout/common/article.jsx +82 -121
- package/layout/common/article_cover.jsx +3 -3
- package/layout/common/article_media.jsx +9 -2
- package/layout/common/footer.jsx +4 -1
- package/layout/common/navbar.jsx +15 -12
- package/layout/common/scripts.jsx +1 -1
- package/layout/layout.jsx +12 -11
- package/layout/tag.jsx +3 -70
- package/layout/tags.jsx +26 -23
- package/package.json +4 -4
- package/source/css/archive.css +287 -166
- package/source/css/default.css +102 -80
- package/source/css/responsive.css +1 -45
- package/source/css/tags.css +53 -59
- package/source/js/components/archive-popup.js +313 -0
- package/source/js/components/friends-list.js +0 -1
- package/source/js/main.js +14 -13
- package/include/hexo/generator/home.js +0 -64
- package/layout/index.jsx +0 -19
- package/layout/misc/paginator.jsx +0 -69
- package/source/js/host/iconify-icon/3.0.2/iconify-icon.min.js +0 -12
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
const pagination = require("hexo-pagination");
|
|
2
|
-
const { filterByLanguage, getLanguage, getLanguageBasePath, getLanguageKeys, isI18nEnabled } = require("../../util/i18n");
|
|
2
|
+
const { filterByLanguage, getDefaultLanguageKey, getLanguage, getLanguageBasePath, getLanguageKeys, isI18nEnabled } = require("../../util/i18n");
|
|
3
3
|
|
|
4
4
|
const fmtNum = (num) => num.toString().padStart(2, "0");
|
|
5
5
|
|
|
6
|
+
function redirectTo(path) {
|
|
7
|
+
const target = path.startsWith("/") ? path : `/${path}`;
|
|
8
|
+
return `<!doctype html><html><head><meta charset="utf-8"><meta name="robots" content="noindex"><meta http-equiv="refresh" content="0;url=${target}"><link rel="canonical" href="${target}"></head><body><a href="${target}">Continue</a></body></html>`;
|
|
9
|
+
}
|
|
10
|
+
|
|
6
11
|
module.exports = (hexo) => {
|
|
7
12
|
hexo.extend.generator.register("archive", function (locals) {
|
|
8
13
|
const { config } = this;
|
|
@@ -44,6 +49,7 @@ module.exports = (hexo) => {
|
|
|
44
49
|
: {};
|
|
45
50
|
|
|
46
51
|
generate(baseArchiveDir, allPosts, languageData);
|
|
52
|
+
generate(languageBase || "", allPosts, languageData);
|
|
47
53
|
|
|
48
54
|
const yearly = themeConfig.yearly ?? true;
|
|
49
55
|
const monthly = themeConfig.monthly ?? true;
|
|
@@ -106,6 +112,13 @@ module.exports = (hexo) => {
|
|
|
106
112
|
}
|
|
107
113
|
|
|
108
114
|
if (isI18nEnabled(fullConfig)) {
|
|
115
|
+
const defaultLanguageBase = getLanguageBasePath(fullConfig, getDefaultLanguageKey(fullConfig));
|
|
116
|
+
if (defaultLanguageBase) {
|
|
117
|
+
result.push({
|
|
118
|
+
path: "index.html",
|
|
119
|
+
data: redirectTo(defaultLanguageBase),
|
|
120
|
+
});
|
|
121
|
+
}
|
|
109
122
|
getLanguageKeys(fullConfig).forEach((langKey) => generateLanguageArchives(langKey));
|
|
110
123
|
return result;
|
|
111
124
|
}
|
|
@@ -1,11 +1,25 @@
|
|
|
1
|
-
const {
|
|
1
|
+
const {
|
|
2
|
+
getI18nKey,
|
|
3
|
+
getLanguage,
|
|
4
|
+
getLanguageBasePath,
|
|
5
|
+
getPageLanguageKey,
|
|
6
|
+
inferI18nKeyFromSource,
|
|
7
|
+
isI18nEnabled,
|
|
8
|
+
localizePath,
|
|
9
|
+
parseLocalizedSource,
|
|
10
|
+
trimSlashes,
|
|
11
|
+
} = require("../../util/i18n");
|
|
2
12
|
|
|
3
13
|
function getLocalizedPagePath(page, langKey, config) {
|
|
4
|
-
const sourcePrefix = `${trimSlashes(langKey)}/`;
|
|
5
14
|
let route = page.path || "";
|
|
6
15
|
|
|
7
|
-
|
|
8
|
-
|
|
16
|
+
const parsed = parseLocalizedSource(page.source || "");
|
|
17
|
+
if (parsed.langKey) {
|
|
18
|
+
const suffix = `__${parsed.langKey}`;
|
|
19
|
+
const escapedSuffix = suffix.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
20
|
+
// Strip the __<lang> suffix when followed by '/', '.', or end of string
|
|
21
|
+
// e.g. "about__en/index.html" → "about/index.html"; "about__en.html" → "about.html"
|
|
22
|
+
route = route.replace(new RegExp(`${escapedSuffix}(?=[/.]|$)`), "");
|
|
9
23
|
}
|
|
10
24
|
|
|
11
25
|
return trimSlashes(localizePath(`/${route}`, langKey, config));
|
|
@@ -6,7 +6,7 @@ module.exports = (hexo) => {
|
|
|
6
6
|
const config = this.config;
|
|
7
7
|
const fullConfig = Object.assign({}, config, config.theme_config, hexo.theme.config);
|
|
8
8
|
const themeConfig = hexo.theme.config.tag_generator || {};
|
|
9
|
-
const perPage = themeConfig.per_page ??
|
|
9
|
+
const perPage = themeConfig.per_page ?? 0;
|
|
10
10
|
const orderBy = themeConfig.order_by ?? "-date";
|
|
11
11
|
const paginationDir = config.pagination_dir || "page";
|
|
12
12
|
const tags = locals.tags;
|
package/include/hexo/helper.js
CHANGED
|
@@ -14,7 +14,6 @@ function getCDN(cdn, pkg, version, filename) {
|
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
const {
|
|
17
|
-
getLanguageLabel,
|
|
18
17
|
getPageLanguageKey,
|
|
19
18
|
getPageLocale,
|
|
20
19
|
getLocalizedTagPath,
|
|
@@ -40,9 +39,6 @@ module.exports = (hexo) => {
|
|
|
40
39
|
hexo.extend.helper.register("language_locale", function (page = null) {
|
|
41
40
|
return getPageLocale(page || this.page, this.config);
|
|
42
41
|
});
|
|
43
|
-
hexo.extend.helper.register("language_label", function (keyOrLocale) {
|
|
44
|
-
return getLanguageLabel(this.config, keyOrLocale);
|
|
45
|
-
});
|
|
46
42
|
hexo.extend.helper.register("localized_path", function (targetPath, langKey = null) {
|
|
47
43
|
return localizePath(targetPath, langKey || getPageLanguageKey(this.page, this.config), this.config);
|
|
48
44
|
});
|
package/include/hexo/i18n.js
CHANGED
|
@@ -1,16 +1,11 @@
|
|
|
1
|
-
const { extname } = require("node:path");
|
|
2
|
-
const createPostProcessor = require("hexo/dist/plugins/processor/post");
|
|
3
|
-
const { isHiddenFile, isMatch, isTmpFile } = require("hexo/dist/plugins/processor/common");
|
|
4
1
|
const {
|
|
5
2
|
getI18nKey,
|
|
6
|
-
getLanguageBasePath,
|
|
7
3
|
getLanguage,
|
|
8
|
-
|
|
4
|
+
getLanguageBasePath,
|
|
9
5
|
getPageLanguageKey,
|
|
10
6
|
inferI18nKeyFromSource,
|
|
11
7
|
isI18nEnabled,
|
|
12
|
-
|
|
13
|
-
trimSlashes,
|
|
8
|
+
parseLocalizedSource,
|
|
14
9
|
} = require("../util/i18n");
|
|
15
10
|
|
|
16
11
|
let cachedConfig = null;
|
|
@@ -27,128 +22,23 @@ function getConfig(hexo, locals = {}) {
|
|
|
27
22
|
return cachedConfig;
|
|
28
23
|
}
|
|
29
24
|
|
|
30
|
-
function
|
|
31
|
-
const
|
|
32
|
-
if (!isI18nEnabled(config) || isTmpFile(sourcePath)) return null;
|
|
33
|
-
|
|
34
|
-
const match = sourcePath.replace(/\\/g, "/").match(/^([^/]+)\/(_posts|_drafts)\/(.+)$/);
|
|
35
|
-
if (!match) return null;
|
|
36
|
-
|
|
37
|
-
const langKey = match[1];
|
|
38
|
-
if (!getLanguageKeys(config).includes(langKey)) return null;
|
|
39
|
-
|
|
40
|
-
const postPath = match[3];
|
|
41
|
-
if (isHiddenFile(postPath)) return null;
|
|
42
|
-
|
|
43
|
-
let renderable = hexo.render.isRenderable(sourcePath) && !isMatch(sourcePath, hexo.config.skip_render);
|
|
44
|
-
if (renderable && hexo.config.post_asset_folder) {
|
|
45
|
-
renderable = extname(hexo.config.new_post_name) === extname(sourcePath);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
return {
|
|
49
|
-
i18n_lang: langKey,
|
|
50
|
-
path: postPath,
|
|
51
|
-
published: match[2] === "_posts",
|
|
52
|
-
renderable,
|
|
53
|
-
};
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function getLocalizedPageParams(hexo, sourcePath) {
|
|
57
|
-
const config = getConfig(hexo);
|
|
58
|
-
if (!isI18nEnabled(config) || isTmpFile(sourcePath)) return null;
|
|
59
|
-
|
|
60
|
-
const normalized = sourcePath.replace(/\\/g, "/");
|
|
61
|
-
const match = normalized.match(/^([^/]+)\/(.+)$/);
|
|
62
|
-
if (!match) return null;
|
|
63
|
-
|
|
64
|
-
const langKey = match[1];
|
|
65
|
-
if (!getLanguageKeys(config).includes(langKey)) return null;
|
|
66
|
-
|
|
67
|
-
const pagePath = match[2];
|
|
68
|
-
if (pagePath.startsWith("_posts/") || pagePath.startsWith("_drafts/") || isHiddenFile(pagePath)) return null;
|
|
69
|
-
|
|
70
|
-
const renderable = hexo.render.isRenderable(sourcePath) && !isMatch(sourcePath, hexo.config.skip_render);
|
|
71
|
-
if (!renderable) return null;
|
|
72
|
-
|
|
73
|
-
return {
|
|
74
|
-
i18n_lang: langKey,
|
|
75
|
-
};
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function buildPostI18nUpdate(post, langKey, config) {
|
|
25
|
+
function applyI18nFields(item, config) {
|
|
26
|
+
const langKey = getPageLanguageKey(item, config);
|
|
79
27
|
const language = getLanguage(config, langKey);
|
|
80
|
-
const update = {
|
|
81
|
-
i18n_path: getLanguageBasePath(config, langKey),
|
|
82
|
-
i18n_lang: langKey,
|
|
83
|
-
lang: language.locale,
|
|
84
|
-
language: language.locale,
|
|
85
|
-
};
|
|
86
|
-
|
|
87
|
-
if (!post.i18n_key) {
|
|
88
|
-
update.i18n_key = getI18nKey(post) || inferI18nKeyFromSource(post.source);
|
|
89
|
-
}
|
|
90
28
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
const sourcePrefix = `${trimSlashes(langKey)}/`;
|
|
96
|
-
let route = page.path || "";
|
|
97
|
-
|
|
98
|
-
if (route.startsWith(sourcePrefix)) {
|
|
99
|
-
route = route.slice(sourcePrefix.length);
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
return trimSlashes(localizePath(`/${route}`, langKey, config));
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
function buildPageI18nUpdate(page, langKey, config) {
|
|
106
|
-
const language = getLanguage(config, langKey);
|
|
107
|
-
const update = {
|
|
108
|
-
i18n_path: getLanguageBasePath(config, langKey),
|
|
109
|
-
i18n_lang: langKey,
|
|
110
|
-
lang: language.locale,
|
|
111
|
-
language: language.locale,
|
|
112
|
-
path: getLocalizedPagePath(page, langKey, config),
|
|
113
|
-
};
|
|
29
|
+
item.i18n_lang = item.i18n_lang || langKey;
|
|
30
|
+
item.i18n_path = getLanguageBasePath(config, langKey);
|
|
31
|
+
item.lang = language.locale;
|
|
32
|
+
item.language = language.locale;
|
|
114
33
|
|
|
115
|
-
if (!
|
|
116
|
-
|
|
34
|
+
if (!item.i18n_key) {
|
|
35
|
+
item.i18n_key = getI18nKey(item) || inferI18nKeyFromSource(item.source);
|
|
117
36
|
}
|
|
118
37
|
|
|
119
|
-
return
|
|
38
|
+
return langKey;
|
|
120
39
|
}
|
|
121
40
|
|
|
122
41
|
module.exports = (hexo) => {
|
|
123
|
-
const defaultPostProcessor = createPostProcessor(hexo);
|
|
124
|
-
|
|
125
|
-
hexo.extend.processor.register(
|
|
126
|
-
(sourcePath) => getLocalizedPostParams(hexo, sourcePath),
|
|
127
|
-
(file) =>
|
|
128
|
-
Promise.resolve(defaultPostProcessor.process(file)).then(() => {
|
|
129
|
-
if (!isI18nEnabled(getConfig(hexo))) return null;
|
|
130
|
-
if (!file.params.renderable) return null;
|
|
131
|
-
|
|
132
|
-
const post = hexo.model("Post").findOne({ source: file.path });
|
|
133
|
-
if (!post) return null;
|
|
134
|
-
|
|
135
|
-
return post.update(buildPostI18nUpdate(post, file.params.i18n_lang, getConfig(hexo)));
|
|
136
|
-
}),
|
|
137
|
-
);
|
|
138
|
-
|
|
139
|
-
hexo.extend.processor.register(
|
|
140
|
-
(sourcePath) => getLocalizedPageParams(hexo, sourcePath),
|
|
141
|
-
(file) => {
|
|
142
|
-
if (!isI18nEnabled(getConfig(hexo))) return null;
|
|
143
|
-
if (file.type === "delete") return null;
|
|
144
|
-
|
|
145
|
-
const page = hexo.model("Page").findOne({ source: file.path });
|
|
146
|
-
if (!page) return null;
|
|
147
|
-
|
|
148
|
-
return page.update(buildPageI18nUpdate(page, file.params.i18n_lang, getConfig(hexo)));
|
|
149
|
-
},
|
|
150
|
-
);
|
|
151
|
-
|
|
152
42
|
hexo.extend.filter.register(
|
|
153
43
|
"post_permalink",
|
|
154
44
|
(post) => {
|
|
@@ -156,14 +46,16 @@ module.exports = (hexo) => {
|
|
|
156
46
|
const activeConfig = getConfig(hexo);
|
|
157
47
|
if (!isI18nEnabled(activeConfig)) return post;
|
|
158
48
|
|
|
159
|
-
|
|
160
|
-
const language = getLanguage(activeConfig, langKey);
|
|
49
|
+
applyI18nFields(post, activeConfig);
|
|
161
50
|
|
|
162
|
-
|
|
163
|
-
post.
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
51
|
+
// 让 permalink 模板里的 :name 取到 baseName 而非 foo__en
|
|
52
|
+
if (post.source) {
|
|
53
|
+
const parsed = parseLocalizedSource(post.source);
|
|
54
|
+
if (parsed.langKey) {
|
|
55
|
+
const baseName = inferI18nKeyFromSource(post.source);
|
|
56
|
+
if (baseName) post.slug = baseName;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
167
59
|
|
|
168
60
|
return post;
|
|
169
61
|
},
|
|
@@ -177,14 +69,7 @@ module.exports = (hexo) => {
|
|
|
177
69
|
const activeConfig = getConfig(hexo, locals);
|
|
178
70
|
if (!page || !isI18nEnabled(activeConfig)) return locals;
|
|
179
71
|
|
|
180
|
-
|
|
181
|
-
const language = getLanguage(activeConfig, langKey);
|
|
182
|
-
|
|
183
|
-
page.i18n_lang = page.i18n_lang || langKey;
|
|
184
|
-
page.i18n_path = getLanguageBasePath(activeConfig, langKey);
|
|
185
|
-
page.lang = language.locale;
|
|
186
|
-
page.language = language.locale;
|
|
187
|
-
page.i18n_key = page.i18n_key || getI18nKey(page) || inferI18nKeyFromSource(page.source);
|
|
72
|
+
applyI18nFields(page, activeConfig);
|
|
188
73
|
|
|
189
74
|
return locals;
|
|
190
75
|
},
|
package/include/util/i18n.js
CHANGED
|
@@ -1,17 +1,6 @@
|
|
|
1
1
|
const path = require("node:path");
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
cn: {
|
|
5
|
-
label: "Chinese",
|
|
6
|
-
locale: "zh-CN",
|
|
7
|
-
prefix: "cn",
|
|
8
|
-
},
|
|
9
|
-
en: {
|
|
10
|
-
label: "English",
|
|
11
|
-
locale: "en",
|
|
12
|
-
prefix: "en",
|
|
13
|
-
},
|
|
14
|
-
};
|
|
3
|
+
// ─── 基础字符串工具 ────────────────────────────────────────────────
|
|
15
4
|
|
|
16
5
|
function trimSlashes(value) {
|
|
17
6
|
return String(value || "").replace(/^\/+|\/+$/g, "");
|
|
@@ -34,6 +23,8 @@ function isExternalUrl(value) {
|
|
|
34
23
|
return /^(?:[a-z][a-z\d+.-]*:)?\/\//i.test(value) || /^(?:mailto|tel|data):/i.test(value) || value.startsWith("#");
|
|
35
24
|
}
|
|
36
25
|
|
|
26
|
+
// ─── i18n 配置解析(带缓存)─────────────────────────────────────────
|
|
27
|
+
|
|
37
28
|
const i18nConfigCache = new WeakMap();
|
|
38
29
|
|
|
39
30
|
function getI18nConfig(config = {}) {
|
|
@@ -41,27 +32,23 @@ function getI18nConfig(config = {}) {
|
|
|
41
32
|
return i18nConfigCache.get(config);
|
|
42
33
|
}
|
|
43
34
|
|
|
44
|
-
const
|
|
45
|
-
const
|
|
46
|
-
const raw = configI18n.enabled === true || configI18n.languages ? configI18n : themeI18n.enabled === true || themeI18n.languages ? themeI18n : configI18n;
|
|
47
|
-
const rawLanguages = raw.languages || DEFAULT_LANGUAGES;
|
|
35
|
+
const raw = config.i18n || {};
|
|
36
|
+
const rawLanguages = raw.languages || {};
|
|
48
37
|
const languages = {};
|
|
49
38
|
|
|
50
39
|
Object.keys(rawLanguages).forEach((key) => {
|
|
51
40
|
const normalizedKey = normalizeLanguageKey(key);
|
|
52
|
-
const value =
|
|
53
|
-
const locale = normalizeLocale(value.locale || value.lang || value.language || normalizedKey);
|
|
54
|
-
const prefix = value.prefix ?? value.path ?? value.url_prefix;
|
|
41
|
+
const value = rawLanguages[key] || {};
|
|
55
42
|
languages[normalizedKey] = {
|
|
56
43
|
key: normalizedKey,
|
|
57
|
-
label: value.label ||
|
|
58
|
-
locale,
|
|
59
|
-
prefix: normalizePrefix(prefix, normalizedKey),
|
|
44
|
+
label: value.label || normalizedKey,
|
|
45
|
+
locale: normalizeLocale(value.locale || normalizedKey),
|
|
46
|
+
prefix: normalizePrefix(value.prefix, normalizedKey),
|
|
60
47
|
};
|
|
61
48
|
});
|
|
62
49
|
|
|
63
50
|
const keys = Object.keys(languages);
|
|
64
|
-
const configuredDefault = normalizeLanguageKey(raw.default ||
|
|
51
|
+
const configuredDefault = normalizeLanguageKey(raw.default || keys[0]);
|
|
65
52
|
const defaultLanguage = languages[configuredDefault] ? configuredDefault : keys[0];
|
|
66
53
|
|
|
67
54
|
const result = {
|
|
@@ -92,22 +79,23 @@ function getLanguage(config = {}, key) {
|
|
|
92
79
|
return i18n.languages[normalizedKey] || i18n.languages[i18n.defaultLanguage];
|
|
93
80
|
}
|
|
94
81
|
|
|
95
|
-
function
|
|
96
|
-
|
|
97
|
-
|
|
82
|
+
function getLanguageBasePath(config, key) {
|
|
83
|
+
return getLanguage(config, key)?.prefix || "";
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ─── 语言键查询(私有辅助)─────────────────────────────────────────
|
|
87
|
+
|
|
88
|
+
function getLanguageKeyFromLocale(config, locale) {
|
|
89
|
+
const normalized = normalizeLocale(locale).toLowerCase();
|
|
90
|
+
if (!normalized) return null;
|
|
98
91
|
|
|
99
92
|
const i18n = getI18nConfig(config);
|
|
100
|
-
if (i18n.languages[
|
|
93
|
+
if (i18n.languages[normalized]) return normalized;
|
|
101
94
|
|
|
102
|
-
return (
|
|
103
|
-
Object.keys(i18n.languages).find((key) => {
|
|
104
|
-
const language = i18n.languages[key];
|
|
105
|
-
return language.locale.toLowerCase() === normalizedLocale;
|
|
106
|
-
}) || null
|
|
107
|
-
);
|
|
95
|
+
return Object.keys(i18n.languages).find((key) => i18n.languages[key].locale.toLowerCase() === normalized) || null;
|
|
108
96
|
}
|
|
109
97
|
|
|
110
|
-
function getLanguageKeyFromPath(value, config
|
|
98
|
+
function getLanguageKeyFromPath(value, config) {
|
|
111
99
|
const normalized = trimSlashes(value);
|
|
112
100
|
if (!normalized) return null;
|
|
113
101
|
|
|
@@ -120,30 +108,69 @@ function getLanguageKeyFromPath(value, config = {}) {
|
|
|
120
108
|
);
|
|
121
109
|
}
|
|
122
110
|
|
|
123
|
-
|
|
124
|
-
const normalized = trimSlashes(value).replace(/\\/g, "/");
|
|
125
|
-
if (!normalized) return null;
|
|
111
|
+
// ─── 源路径解析:从文件名识别 __<lang> 后缀 ──────────────────────────
|
|
126
112
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
113
|
+
function parseLocalizedSource(source) {
|
|
114
|
+
if (typeof source !== "string" || !source) {
|
|
115
|
+
return { langKey: null, baseSource: "" };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const normalized = source.replace(/\\/g, "/");
|
|
119
|
+
const lastSlash = normalized.lastIndexOf("/");
|
|
120
|
+
const dir = lastSlash >= 0 ? normalized.slice(0, lastSlash + 1) : "";
|
|
121
|
+
const filename = lastSlash >= 0 ? normalized.slice(lastSlash + 1) : normalized;
|
|
122
|
+
const ext = path.posix.extname(filename);
|
|
123
|
+
const stem = ext ? filename.slice(0, -ext.length) : filename;
|
|
124
|
+
|
|
125
|
+
const match = stem.match(/^(.+?)__([a-zA-Z][\w-]*)$/);
|
|
126
|
+
if (!match) return { langKey: null, baseSource: normalized };
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
langKey: match[2],
|
|
130
|
+
baseSource: dir + match[1] + ext,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function inferI18nKeyFromSource(source) {
|
|
135
|
+
if (typeof source !== "string") return "";
|
|
136
|
+
const { baseSource } = parseLocalizedSource(source);
|
|
137
|
+
const normalized = trimSlashes(baseSource || source).replace(/\\/g, "/");
|
|
138
|
+
const ext = path.posix.extname(normalized);
|
|
139
|
+
const withoutExt = ext ? normalized.slice(0, -ext.length) : normalized;
|
|
140
|
+
const parts = withoutExt.split("/").filter(Boolean);
|
|
141
|
+
if (!parts.length) return "";
|
|
142
|
+
|
|
143
|
+
const last = parts[parts.length - 1];
|
|
144
|
+
if (last === "index" && parts.length >= 2) {
|
|
145
|
+
return parts[parts.length - 2];
|
|
146
|
+
}
|
|
147
|
+
return last;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function getI18nKey(item = {}) {
|
|
151
|
+
if (item.i18n_key) return item.i18n_key;
|
|
152
|
+
|
|
153
|
+
const fromSource = inferI18nKeyFromSource(item.source);
|
|
154
|
+
if (fromSource) return fromSource;
|
|
155
|
+
|
|
156
|
+
return item.slug || "";
|
|
130
157
|
}
|
|
131
158
|
|
|
159
|
+
// ─── 单个 post / page 的语言归属查询 ────────────────────────────────
|
|
160
|
+
|
|
132
161
|
function getPageLanguageKey(page = {}, config = {}) {
|
|
133
162
|
if (!isI18nEnabled(config)) {
|
|
134
|
-
const
|
|
135
|
-
return getLanguageKeyFromLocale(config, page.lang || page.language ||
|
|
163
|
+
const fallback = Array.isArray(config.language) ? config.language[0] : config.language;
|
|
164
|
+
return getLanguageKeyFromLocale(config, page.lang || page.language || fallback) || getDefaultLanguageKey(config);
|
|
136
165
|
}
|
|
137
166
|
|
|
138
|
-
const explicit = normalizeLanguageKey(page.i18n_lang
|
|
167
|
+
const explicit = normalizeLanguageKey(page.i18n_lang);
|
|
139
168
|
if (explicit && getLanguage(config, explicit)?.key === explicit) return explicit;
|
|
140
169
|
|
|
141
|
-
const fromSource =
|
|
170
|
+
const fromSource = parseLocalizedSource(page.source || page.full_source || "").langKey;
|
|
142
171
|
if (fromSource) return fromSource;
|
|
143
172
|
|
|
144
|
-
const
|
|
145
|
-
const pathValue = pathDescriptor && typeof pathDescriptor.get !== "function" ? pathDescriptor.value : "";
|
|
146
|
-
const fromPath = getLanguageKeyFromPath(pathValue || page.canonical_path || "", config);
|
|
173
|
+
const fromPath = getLanguageKeyFromPath(page.path || page.canonical_path || "", config);
|
|
147
174
|
if (fromPath) return fromPath;
|
|
148
175
|
|
|
149
176
|
const fromLocale = getLanguageKeyFromLocale(config, page.lang || page.language);
|
|
@@ -154,30 +181,22 @@ function getPageLanguageKey(page = {}, config = {}) {
|
|
|
154
181
|
|
|
155
182
|
function getPageLocale(page = {}, config = {}) {
|
|
156
183
|
if (!isI18nEnabled(config)) {
|
|
157
|
-
const
|
|
158
|
-
return normalizeLocale(page.lang || page.language ||
|
|
184
|
+
const fallback = Array.isArray(config.language) ? config.language[0] : config.language;
|
|
185
|
+
return normalizeLocale(page.lang || page.language || fallback || "");
|
|
159
186
|
}
|
|
160
187
|
|
|
161
188
|
const language = getLanguage(config, getPageLanguageKey(page, config));
|
|
162
189
|
return language?.locale || normalizeLocale(page.lang || page.language) || "";
|
|
163
190
|
}
|
|
164
191
|
|
|
165
|
-
|
|
166
|
-
const key = getLanguage(config, keyOrLocale)?.key === normalizeLanguageKey(keyOrLocale) ? normalizeLanguageKey(keyOrLocale) : getLanguageKeyFromLocale(config, keyOrLocale);
|
|
167
|
-
const language = getLanguage(config, key || getDefaultLanguageKey(config));
|
|
168
|
-
return language?.label || keyOrLocale;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
function getLanguageBasePath(config = {}, key) {
|
|
172
|
-
return getLanguage(config, key)?.prefix || "";
|
|
173
|
-
}
|
|
192
|
+
// ─── 路径生成 ──────────────────────────────────────────────────────
|
|
174
193
|
|
|
175
194
|
function joinRoute(...parts) {
|
|
176
195
|
const joined = parts.map(trimSlashes).filter(Boolean).join("/");
|
|
177
196
|
return joined ? `${joined}/` : "";
|
|
178
197
|
}
|
|
179
198
|
|
|
180
|
-
function stripLanguagePrefix(value, config
|
|
199
|
+
function stripLanguagePrefix(value, config) {
|
|
181
200
|
const normalized = trimSlashes(value);
|
|
182
201
|
const key = getLanguageKeyFromPath(normalized, config);
|
|
183
202
|
if (!key) return normalized;
|
|
@@ -193,35 +212,12 @@ function localizePath(value, key, config = {}) {
|
|
|
193
212
|
|
|
194
213
|
const [pathAndQuery, hash = ""] = value.split("#");
|
|
195
214
|
const [pathname, query = ""] = pathAndQuery.split("?");
|
|
196
|
-
const route = stripLanguagePrefix(pathname, config);
|
|
215
|
+
const route = trimSlashes(stripLanguagePrefix(pathname, config));
|
|
197
216
|
const base = trimSlashes(getLanguageBasePath(config, key || getDefaultLanguageKey(config)));
|
|
198
|
-
const
|
|
199
|
-
const
|
|
200
|
-
const hasFileExtension = /\.[^/]+$/.test(normalizedRoute);
|
|
217
|
+
const joined = [base, route].filter(Boolean).join("/");
|
|
218
|
+
const hasFileExtension = /\.[^/]+$/.test(route);
|
|
201
219
|
const localized = `/${joined}${joined && !hasFileExtension ? "/" : ""}`;
|
|
202
|
-
|
|
203
|
-
const hashPart = hash ? `#${hash}` : "";
|
|
204
|
-
return `${localized}${queryPart}${hashPart}`;
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
function toArray(collection) {
|
|
208
|
-
if (!collection) return [];
|
|
209
|
-
if (typeof collection.toArray === "function") return collection.toArray();
|
|
210
|
-
if (Array.isArray(collection.data)) return collection.data;
|
|
211
|
-
if (Array.isArray(collection)) return collection;
|
|
212
|
-
return [];
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
function filterByLanguage(collection, key, config = {}) {
|
|
216
|
-
if (!isI18nEnabled(config)) return collection;
|
|
217
|
-
if (typeof collection?.filter === "function" && !Array.isArray(collection)) {
|
|
218
|
-
return collection.filter((item) => getPageLanguageKey(item, config) === key);
|
|
219
|
-
}
|
|
220
|
-
return toArray(collection).filter((item) => getPageLanguageKey(item, config) === key);
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
function getI18nKey(item = {}) {
|
|
224
|
-
return item.i18n_key || item.i18n?.key || item.translation_key || item.slug || inferI18nKeyFromSource(item.source);
|
|
220
|
+
return `${localized}${query ? `?${query}` : ""}${hash ? `#${hash}` : ""}`;
|
|
225
221
|
}
|
|
226
222
|
|
|
227
223
|
function getLocalizedTagPath(tag, key, config = {}) {
|
|
@@ -237,44 +233,34 @@ function getLocalizedTagPath(tag, key, config = {}) {
|
|
|
237
233
|
return joinRoute(getLanguageBasePath(config, key), tagDir, slug);
|
|
238
234
|
}
|
|
239
235
|
|
|
240
|
-
|
|
241
|
-
if (typeof source !== "string") return "";
|
|
242
|
-
const normalized = trimSlashes(source).replace(/\\/g, "/");
|
|
243
|
-
const ext = path.posix.extname(normalized);
|
|
244
|
-
const withoutExt = ext ? normalized.slice(0, -ext.length) : normalized;
|
|
245
|
-
const parts = withoutExt.split("/").filter(Boolean);
|
|
246
|
-
if (!parts.length) return "";
|
|
236
|
+
// ─── 集合按语言筛选 ────────────────────────────────────────────────
|
|
247
237
|
|
|
248
|
-
|
|
249
|
-
if (
|
|
250
|
-
|
|
238
|
+
function filterByLanguage(collection, key, config = {}) {
|
|
239
|
+
if (!isI18nEnabled(config)) return collection;
|
|
240
|
+
// Hexo Query 对象:保留链式能力
|
|
241
|
+
if (typeof collection?.filter === "function" && !Array.isArray(collection)) {
|
|
242
|
+
return collection.filter((item) => getPageLanguageKey(item, config) === key);
|
|
251
243
|
}
|
|
252
|
-
|
|
253
|
-
|
|
244
|
+
// 普通数组 / 含 toArray 的对象
|
|
245
|
+
const arr = Array.isArray(collection) ? collection : typeof collection?.toArray === "function" ? collection.toArray() : [];
|
|
246
|
+
return arr.filter((item) => getPageLanguageKey(item, config) === key);
|
|
254
247
|
}
|
|
255
248
|
|
|
256
249
|
module.exports = {
|
|
257
250
|
filterByLanguage,
|
|
258
251
|
getDefaultLanguageKey,
|
|
259
|
-
getI18nConfig,
|
|
260
252
|
getI18nKey,
|
|
261
253
|
getLanguage,
|
|
262
254
|
getLanguageBasePath,
|
|
263
|
-
getLanguageKeyFromLocale,
|
|
264
|
-
getLanguageKeyFromPath,
|
|
265
|
-
getLanguageKeyFromSource,
|
|
266
255
|
getLanguageKeys,
|
|
267
|
-
getLanguageLabel,
|
|
268
256
|
getLocalizedTagPath,
|
|
269
257
|
getPageLanguageKey,
|
|
270
258
|
getPageLocale,
|
|
271
259
|
inferI18nKeyFromSource,
|
|
272
260
|
isExternalUrl,
|
|
273
261
|
isI18nEnabled,
|
|
274
|
-
joinRoute,
|
|
275
262
|
localizePath,
|
|
276
263
|
normalizeLocale,
|
|
277
|
-
|
|
278
|
-
toArray,
|
|
264
|
+
parseLocalizedSource,
|
|
279
265
|
trimSlashes,
|
|
280
266
|
};
|