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.
@@ -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
  }
@@ -73,10 +73,5 @@ module.exports = (hexo) => {
73
73
  require("./archive")(hexo);
74
74
  }
75
75
 
76
- const indexConfig = hexo.theme.config.index_generator || {};
77
- if (indexConfig.enabled !== false) {
78
- require("./home")(hexo);
79
- }
80
-
81
76
  require("./page")(hexo);
82
77
  };
@@ -1,11 +1,25 @@
1
- const { getI18nKey, getLanguage, getLanguageBasePath, getPageLanguageKey, inferI18nKeyFromSource, isI18nEnabled, localizePath, trimSlashes } = require("../../util/i18n");
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
- if (route.startsWith(sourcePrefix)) {
8
- route = route.slice(sourcePrefix.length);
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 ?? 10;
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;
@@ -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
  });
@@ -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
- getLanguageKeys,
4
+ getLanguageBasePath,
9
5
  getPageLanguageKey,
10
6
  inferI18nKeyFromSource,
11
7
  isI18nEnabled,
12
- localizePath,
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 getLocalizedPostParams(hexo, sourcePath) {
31
- const config = getConfig(hexo);
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
- return { $set: update };
92
- }
93
-
94
- function getLocalizedPagePath(page, langKey, config) {
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 (!page.i18n_key) {
116
- update.i18n_key = getI18nKey(page) || inferI18nKeyFromSource(page.source);
34
+ if (!item.i18n_key) {
35
+ item.i18n_key = getI18nKey(item) || inferI18nKeyFromSource(item.source);
117
36
  }
118
37
 
119
- return { $set: update };
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
- const langKey = getPageLanguageKey(post, activeConfig);
160
- const language = getLanguage(activeConfig, langKey);
49
+ applyI18nFields(post, activeConfig);
161
50
 
162
- post.i18n_lang = post.i18n_lang || langKey;
163
- post.i18n_path = getLanguageBasePath(activeConfig, langKey);
164
- post.lang = language.locale;
165
- post.language = language.locale;
166
- post.i18n_key = post.i18n_key || getI18nKey(post) || inferI18nKeyFromSource(post.source);
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
- const langKey = getPageLanguageKey(page, activeConfig);
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
  },
@@ -1,17 +1,6 @@
1
1
  const path = require("node:path");
2
2
 
3
- const DEFAULT_LANGUAGES = {
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 configI18n = config.i18n || {};
45
- const themeI18n = config.theme_config?.i18n || {};
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 = typeof rawLanguages[key] === "string" ? { locale: rawLanguages[key] } : rawLanguages[key] || {};
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 || value.name || locale || normalizedKey,
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 || raw.default_language || raw.defaultLanguage || keys[0]);
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 getLanguageKeyFromLocale(config = {}, locale) {
96
- const normalizedLocale = normalizeLocale(locale).toLowerCase();
97
- if (!normalizedLocale) return null;
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[normalizedLocale]) return normalizedLocale;
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
- function getLanguageKeyFromSource(value, config = {}) {
124
- const normalized = trimSlashes(value).replace(/\\/g, "/");
125
- if (!normalized) return null;
111
+ // ─── 源路径解析:从文件名识别 __<lang> 后缀 ──────────────────────────
126
112
 
127
- const firstSegment = normalized.split("/")[0];
128
- const i18n = getI18nConfig(config);
129
- return i18n.languages[firstSegment] ? firstSegment : null;
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 configuredLanguage = Array.isArray(config.language) ? config.language[0] : config.language;
135
- return getLanguageKeyFromLocale(config, page.lang || page.language || configuredLanguage) || normalizeLanguageKey(page.lang || page.language || configuredLanguage) || getDefaultLanguageKey(config);
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 || page.i18n?.lang || page.i18n?.language);
167
+ const explicit = normalizeLanguageKey(page.i18n_lang);
139
168
  if (explicit && getLanguage(config, explicit)?.key === explicit) return explicit;
140
169
 
141
- const fromSource = getLanguageKeyFromSource(page.source || page.full_source || "", config);
170
+ const fromSource = parseLocalizedSource(page.source || page.full_source || "").langKey;
142
171
  if (fromSource) return fromSource;
143
172
 
144
- const pathDescriptor = Object.getOwnPropertyDescriptor(page, "path");
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 configuredLanguage = Array.isArray(config.language) ? config.language[0] : config.language;
158
- return normalizeLocale(page.lang || page.language || configuredLanguage || "");
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
- function getLanguageLabel(config = {}, keyOrLocale) {
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 normalizedRoute = trimSlashes(route);
199
- const joined = [base, normalizedRoute].filter(Boolean).join("/");
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
- const queryPart = query ? `?${query}` : "";
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
- function inferI18nKeyFromSource(source) {
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
- const last = parts[parts.length - 1];
249
- if (last === "index" && parts.length >= 2) {
250
- return parts[parts.length - 2];
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
- return last;
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
- stripLanguagePrefix,
278
- toArray,
264
+ parseLocalizedSource,
279
265
  trimSlashes,
280
266
  };