valaxy 0.28.0-beta.6 → 0.28.0-beta.7

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,12 +1,12 @@
1
1
  import type { Ref } from 'vue'
2
+ import type { TaxonomyNamespace } from '../../shared/utils/i18n'
2
3
  import { isClient, useStorage } from '@vueuse/core'
3
4
  import dayjs from 'dayjs'
4
- import { computed } from 'vue'
5
5
 
6
+ import { computed, watch } from 'vue'
6
7
  // not optimize deps all locales
7
8
  import { useI18n } from 'vue-i18n'
8
- import { tObject } from '../../shared/utils/i18n'
9
- import { LOCALE_PREFIX } from '../utils'
9
+ import { isLocaleKey, resolveTaxonomyLocaleKey, stripLocalePrefix, tObject } from '../../shared/utils/i18n'
10
10
  import 'dayjs/locale/en'
11
11
  import 'dayjs/locale/zh-cn'
12
12
 
@@ -63,13 +63,16 @@ export function useLocale() {
63
63
  export function useLocaleTitle(fm: Ref<{
64
64
  title?: string | Record<string, string>
65
65
  } | null>) {
66
- const { locale } = useI18n()
66
+ const { t, locale } = useI18n()
67
67
  return computed(() => {
68
68
  if (!fm.value)
69
69
  return ''
70
70
 
71
71
  const lang = locale.value
72
- return tObject(fm.value.title || '', lang) || ''
72
+ const title = tObject(fm.value.title || '', lang) || ''
73
+ if (typeof title === 'string' && isLocaleKey(title))
74
+ return t(stripLocalePrefix(title))
75
+ return title
73
76
  })
74
77
  }
75
78
 
@@ -79,16 +82,19 @@ export function useLocaleTitle(fm: Ref<{
79
82
  * 会从 locales/ 目录中获取对应的翻译
80
83
  */
81
84
  export function useValaxyI18n() {
82
- const { t, locale } = useI18n()
85
+ const { t, te, locale } = useI18n()
86
+ const termCache = new Map<string, string>()
87
+
88
+ // Clear cache on locale switches so each composable instance stays bounded.
89
+ watch(locale, () => termCache.clear())
83
90
 
84
91
  /**
85
92
  * translate `$locale:key`
86
93
  * @param key
87
94
  */
88
95
  const $t = (key: string) => {
89
- if (key.startsWith(LOCALE_PREFIX)) {
90
- return t(key.slice(LOCALE_PREFIX.length))
91
- }
96
+ if (isLocaleKey(key))
97
+ return t(stripLocalePrefix(key))
92
98
  return key
93
99
  }
94
100
 
@@ -104,6 +110,47 @@ export function useValaxyI18n() {
104
110
  return tObject(data || '', locale.value)
105
111
  }
106
112
 
113
+ /**
114
+ * @en
115
+ * Translate a taxonomy term.
116
+ *
117
+ * Resolution order:
118
+ * 1. `$locale:` prefix → strip and translate via `t()`
119
+ * 2. Locale key `{namespace}.{key}` exists → translate via `t()`
120
+ * 3. Fallback → return the original key as-is
121
+ *
122
+ * The result is cached by `locale + namespace + key` to avoid repeated
123
+ * `te()` / `t()` lookups in tag clouds and category trees.
124
+ *
125
+ * @zh
126
+ * 翻译 taxonomy 术语。
127
+ *
128
+ * 解析顺序:
129
+ * 1. `$locale:` 前缀 → 去掉前缀后通过 `t()` 翻译
130
+ * 2. locale 中存在 `{namespace}.{key}` → 通过 `t()` 翻译
131
+ * 3. 兜底 → 原样返回
132
+ *
133
+ * 结果会按 `locale + namespace + key` 做轻量缓存,
134
+ * 避免标签云和分类树中重复执行 `te()` / `t()`。
135
+ */
136
+ const $tTerm = (namespace: TaxonomyNamespace, key: string) => {
137
+ const cacheKey = `${locale.value}:${namespace}:${key}`
138
+ const cached = termCache.get(cacheKey)
139
+ if (cached !== undefined)
140
+ return cached
141
+
142
+ const { localeKey, isExplicitLocaleKey } = resolveTaxonomyLocaleKey(namespace, key)
143
+ const result = isExplicitLocaleKey || te(localeKey)
144
+ ? `${t(localeKey)}`
145
+ : key
146
+
147
+ termCache.set(cacheKey, result)
148
+ return result
149
+ }
150
+
151
+ const $tTag = (key: string) => $tTerm('tag', key)
152
+ const $tCategory = (key: string) => $tTerm('category', key)
153
+
107
154
  return {
108
155
  locale,
109
156
  /**
@@ -111,5 +158,17 @@ export function useValaxyI18n() {
111
158
  */
112
159
  $t,
113
160
  $tO,
161
+ /**
162
+ * translate taxonomy term (auto-lookup `{namespace}.{key}` in locale files)
163
+ */
164
+ $tTerm,
165
+ /**
166
+ * translate tag name (auto-lookup `tag.{key}` in locale files)
167
+ */
168
+ $tTag,
169
+ /**
170
+ * translate category name (auto-lookup `category.{key}` in locale files)
171
+ */
172
+ $tCategory,
114
173
  }
115
174
  }
@@ -6,15 +6,18 @@ import { orderByMeta, useSiteConfig } from 'valaxy'
6
6
  import { computed } from 'vue'
7
7
  import { useI18n } from 'vue-i18n'
8
8
  import { useRouterStore } from '../../stores'
9
- import { tObject } from '../../utils'
9
+ import { isLocaleKey, stripLocalePrefix, tObject } from '../../utils'
10
10
 
11
11
  export * from './usePagination'
12
12
  export * from './usePrevNext'
13
13
 
14
14
  export function usePostTitle(post: ComputedRef<Post>) {
15
- const { locale } = useI18n()
15
+ const { t, locale } = useI18n()
16
16
  return computed(() => {
17
- return tObject(post.value.title || '', locale.value)
17
+ const title = tObject(post.value.title || '', locale.value)
18
+ if (typeof title === 'string' && isLocaleKey(title))
19
+ return t(stripLocalePrefix(title))
20
+ return title
18
21
  })
19
22
  }
20
23
 
@@ -1,7 +1,7 @@
1
1
  import 'node:process';
2
2
  import 'yargs';
3
3
  import 'yargs/helpers';
4
- export { c as cli, I as registerDevCommand, W as run, Z as startValaxyDev } from '../../shared/valaxy.BVsZMcdc.mjs';
4
+ export { c as cli, I as registerDevCommand, W as run, Z as startValaxyDev } from '../../shared/valaxy.DAkHYbg0.mjs';
5
5
  import 'node:os';
6
6
  import 'node:path';
7
7
  import 'consola';
@@ -694,6 +694,30 @@ interface ValaxyExtendConfig {
694
694
  */
695
695
  maxDuration?: number;
696
696
  };
697
+ /**
698
+ * @en Taxonomy i18n validation during `valaxy dev` / `valaxy build`.
699
+ * Checks whether translated `tag.*` / `category.*` keys are consistently
700
+ * defined across configured languages.
701
+ *
702
+ * @zh `valaxy dev` / `valaxy build` 期间的 taxonomy i18n 校验。
703
+ * 用于检查 `tag.*` / `category.*` 翻译 key 是否在已配置语言中保持一致。
704
+ */
705
+ taxonomyI18n?: {
706
+ /**
707
+ * @en Validation level for taxonomy i18n checks.
708
+ * - `'off'`: disable checks
709
+ * - `'warn'`: print warnings and continue
710
+ * - `'error'`: fail validation after reporting all issues
711
+ *
712
+ * @zh taxonomy i18n 校验级别。
713
+ * - `'off'`:关闭检查
714
+ * - `'warn'`:输出 warning 并继续流程
715
+ * - `'error'`:输出所有问题后以错误结束
716
+ *
717
+ * @default 'warn'
718
+ */
719
+ level?: 'off' | 'warn' | 'error';
720
+ };
697
721
  };
698
722
  /**
699
723
  * @experimental
@@ -1,4 +1,4 @@
1
- export { A as ALL_ROUTE, E as EXCERPT_SEPARATOR, G as GLOBAL_STATE, P as PATHNAME_PROTOCOL_RE, V as ViteValaxyPlugins, b as build, c as cli, a as createServer, d as createValaxyPlugin, e as customElements, f as defaultSiteConfig, g as defaultValaxyConfig, h as defaultViteConfig, i as defineAddon, j as defineConfig, k as defineSiteConfig, l as defineTheme, m as defineValaxyAddon, n as defineValaxyConfig, o as defineValaxyTheme, p as encryptContent, q as generateClientRedirects, r as getGitTimestamp, s as getIndexHtml, t as getServerInfoText, u as isExternal, v as isInstalledGlobally, w as isKatexEnabled, x as isKatexPluginNeeded, y as isMathJaxEnabled, z as isPath, B as loadConfigFromFile, C as mergeValaxyConfig, D as mergeViteConfigs, F as postProcessForSSG, H as processValaxyOptions, I as registerDevCommand, J as resolveAddonsConfig, K as resolveImportPath, L as resolveImportUrl, M as resolveOptions, N as resolveSiteConfig, O as resolveSiteConfigFromRoot, Q as resolveThemeConfigFromRoot, R as resolveThemeValaxyConfig, S as resolveUserThemeConfig, T as resolveValaxyConfig, U as resolveValaxyConfigFromRoot, W as run, X as ssgBuild, Y as ssgBuildLegacy, Z as startValaxyDev, _ as toAtFS, $ as transformObject, a0 as version } from '../shared/valaxy.BVsZMcdc.mjs';
1
+ export { A as ALL_ROUTE, E as EXCERPT_SEPARATOR, G as GLOBAL_STATE, P as PATHNAME_PROTOCOL_RE, V as ViteValaxyPlugins, b as build, c as cli, a as createServer, d as createValaxyPlugin, e as customElements, f as defaultSiteConfig, g as defaultValaxyConfig, h as defaultViteConfig, i as defineAddon, j as defineConfig, k as defineSiteConfig, l as defineTheme, m as defineValaxyAddon, n as defineValaxyConfig, o as defineValaxyTheme, p as encryptContent, q as generateClientRedirects, r as getGitTimestamp, s as getIndexHtml, t as getServerInfoText, u as isExternal, v as isInstalledGlobally, w as isKatexEnabled, x as isKatexPluginNeeded, y as isMathJaxEnabled, z as isPath, B as loadConfigFromFile, C as mergeValaxyConfig, D as mergeViteConfigs, F as postProcessForSSG, H as processValaxyOptions, I as registerDevCommand, J as resolveAddonsConfig, K as resolveImportPath, L as resolveImportUrl, M as resolveOptions, N as resolveSiteConfig, O as resolveSiteConfigFromRoot, Q as resolveThemeConfigFromRoot, R as resolveThemeValaxyConfig, S as resolveUserThemeConfig, T as resolveValaxyConfig, U as resolveValaxyConfigFromRoot, W as run, X as ssgBuild, Y as ssgBuildLegacy, Z as startValaxyDev, _ as toAtFS, $ as transformObject, a0 as version } from '../shared/valaxy.DAkHYbg0.mjs';
2
2
  import 'node:path';
3
3
  import 'fs-extra';
4
4
  import 'consola/utils';
@@ -854,6 +854,9 @@ const defaultValaxyConfig = {
854
854
  foucGuard: {
855
855
  enabled: true,
856
856
  maxDuration: 5e3
857
+ },
858
+ taxonomyI18n: {
859
+ level: "warn"
857
860
  }
858
861
  },
859
862
  deploy: {},
@@ -985,6 +988,30 @@ function tObject(data, lang) {
985
988
  }
986
989
  return data;
987
990
  }
991
+ function isLocaleKey(value) {
992
+ return value.startsWith(LOCALE_PREFIX);
993
+ }
994
+ function stripLocalePrefix(value) {
995
+ return isLocaleKey(value) ? value.slice(LOCALE_PREFIX.length) : value;
996
+ }
997
+ function getLocaleMessageValue(messages, key) {
998
+ return key.split(".").reduce((result, part) => result?.[part], messages);
999
+ }
1000
+ function hasLocaleMessage(messages, key) {
1001
+ return getLocaleMessageValue(messages, key) !== void 0;
1002
+ }
1003
+ function resolveTaxonomyLocaleKey(namespace, rawValue) {
1004
+ if (isLocaleKey(rawValue)) {
1005
+ return {
1006
+ localeKey: stripLocalePrefix(rawValue),
1007
+ isExplicitLocaleKey: true
1008
+ };
1009
+ }
1010
+ return {
1011
+ localeKey: `${namespace}.${rawValue}`,
1012
+ isExplicitLocaleKey: false
1013
+ };
1014
+ }
988
1015
 
989
1016
  const indexRE = /(^|.*\/)index.md(.*)$/i;
990
1017
  function linkPlugin(md, externalAttrs, base) {
@@ -1692,7 +1719,7 @@ async function setupMarkdownPlugins(md, options, base = "/") {
1692
1719
  return md;
1693
1720
  }
1694
1721
 
1695
- const version = "0.28.0-beta.6";
1722
+ const version = "0.28.0-beta.7";
1696
1723
 
1697
1724
  const GLOBAL_STATE = {
1698
1725
  valaxyApp: void 0,
@@ -2482,9 +2509,8 @@ function loadLocalesYml(localesPath, force = false) {
2482
2509
  return locales;
2483
2510
  }
2484
2511
  function nodeT(key, lang) {
2485
- if (key.startsWith(LOCALE_PREFIX)) {
2486
- key = key.slice(LOCALE_PREFIX.length);
2487
- }
2512
+ if (isLocaleKey(key))
2513
+ key = stripLocalePrefix(key);
2488
2514
  const data = NODE_I18N.locales[lang] || {};
2489
2515
  const keys = key.split(".");
2490
2516
  let result = data;
@@ -2529,7 +2555,7 @@ function getSiteUrl(options) {
2529
2555
  }
2530
2556
 
2531
2557
  function resolveText(value, lang) {
2532
- if (typeof value === "string" && value.startsWith(LOCALE_PREFIX))
2558
+ if (typeof value === "string" && isLocaleKey(value))
2533
2559
  return nodeT(value, lang);
2534
2560
  return tObject(value, lang);
2535
2561
  }
@@ -5650,6 +5676,162 @@ const content = {
5650
5676
  loadAllContent: loadAllContent
5651
5677
  };
5652
5678
 
5679
+ function getConfiguredLanguages(options) {
5680
+ const configured = options.config.siteConfig.languages?.length ? options.config.siteConfig.languages : [options.config.siteConfig.lang || "en"];
5681
+ return [...new Set(configured.filter(Boolean))];
5682
+ }
5683
+ function normalizeToStringArray(value) {
5684
+ if (typeof value === "string")
5685
+ return value.trim() ? [value] : [];
5686
+ if (Array.isArray(value)) {
5687
+ return value.filter((item) => typeof item === "string").map((item) => item.trim()).filter(Boolean);
5688
+ }
5689
+ return [];
5690
+ }
5691
+ async function collectPageFiles(options) {
5692
+ const userPages = await scanPageFiles(options.userRoot, ["**/*.md"]);
5693
+ const userPageFiles = userPages.map((file) => resolve(options.userRoot, "pages", file));
5694
+ const contentDir = resolve(options.tempDir, "content", "pages");
5695
+ if (!await fs.pathExists(contentDir))
5696
+ return userPageFiles;
5697
+ const contentPages = await fg(["**/*.md"], {
5698
+ cwd: contentDir,
5699
+ ignore: ["**/node_modules"]
5700
+ });
5701
+ return [.../* @__PURE__ */ new Set([
5702
+ ...userPageFiles,
5703
+ ...contentPages.map((file) => resolve(contentDir, file))
5704
+ ])];
5705
+ }
5706
+ async function loadMergedLocaleMessages(options) {
5707
+ const languages = getConfiguredLanguages(options);
5708
+ const messages = Object.fromEntries(languages.map((lang) => [lang, {}]));
5709
+ for (const root of options.roots) {
5710
+ for (const lang of languages) {
5711
+ const localeFiles = [
5712
+ resolve(root, "locales", `${lang}.yml`),
5713
+ resolve(root, "locales", `${lang}.yaml`)
5714
+ ];
5715
+ for (const localeFile of localeFiles) {
5716
+ if (!await fs.pathExists(localeFile))
5717
+ continue;
5718
+ const content = await fs.readFile(localeFile, "utf-8");
5719
+ if (!content.trim())
5720
+ continue;
5721
+ const data = yaml.load(content);
5722
+ messages[lang] = replaceArrMerge(data || {}, messages[lang]);
5723
+ }
5724
+ }
5725
+ }
5726
+ return messages;
5727
+ }
5728
+ async function collectTaxonomyUsages(options) {
5729
+ const files = await collectPageFiles(options);
5730
+ const posts = await readPostFiles(files);
5731
+ const usageMap = /* @__PURE__ */ new Map();
5732
+ const addUsage = (namespace, rawValue, file) => {
5733
+ const key = `${namespace}:${rawValue}`;
5734
+ if (!usageMap.has(key)) {
5735
+ usageMap.set(key, {
5736
+ namespace,
5737
+ rawValue,
5738
+ files: /* @__PURE__ */ new Set()
5739
+ });
5740
+ }
5741
+ usageMap.get(key).files.add(file);
5742
+ };
5743
+ for (const post of posts) {
5744
+ const file = relative(options.userRoot, post.filePath);
5745
+ for (const tag of normalizeToStringArray(post.data.tags))
5746
+ addUsage("tag", tag, file);
5747
+ for (const category of normalizeToStringArray(post.data.categories))
5748
+ addUsage("category", category, file);
5749
+ }
5750
+ return Array.from(usageMap.values(), (item) => ({
5751
+ namespace: item.namespace,
5752
+ rawValue: item.rawValue,
5753
+ files: [...item.files].sort()
5754
+ })).sort((a, b) => `${a.namespace}:${a.rawValue}`.localeCompare(`${b.namespace}:${b.rawValue}`));
5755
+ }
5756
+ function findTaxonomyI18nIssues(usages, messages, languages) {
5757
+ const issues = [];
5758
+ for (const usage of usages) {
5759
+ const { namespace, rawValue, files } = usage;
5760
+ const { localeKey, isExplicitLocaleKey } = resolveTaxonomyLocaleKey(namespace, rawValue);
5761
+ const presentLanguages = languages.filter((lang) => hasLocaleMessage(messages[lang], localeKey));
5762
+ const missingLanguages = languages.filter((lang) => !hasLocaleMessage(messages[lang], localeKey));
5763
+ if (isExplicitLocaleKey) {
5764
+ if (missingLanguages.length) {
5765
+ issues.push({
5766
+ type: "explicit-locale-key-missing",
5767
+ namespace,
5768
+ rawValue,
5769
+ localeKey,
5770
+ presentLanguages,
5771
+ missingLanguages,
5772
+ files
5773
+ });
5774
+ }
5775
+ continue;
5776
+ }
5777
+ if (presentLanguages.length === 0 || presentLanguages.length === languages.length)
5778
+ continue;
5779
+ issues.push({
5780
+ type: "partial-locale-coverage",
5781
+ namespace,
5782
+ rawValue,
5783
+ localeKey,
5784
+ presentLanguages,
5785
+ missingLanguages,
5786
+ files
5787
+ });
5788
+ }
5789
+ return issues;
5790
+ }
5791
+ function formatFiles(files) {
5792
+ const preview = files.slice(0, 3).map((file) => colors.dim(file)).join(", ");
5793
+ if (files.length <= 3)
5794
+ return preview;
5795
+ return `${preview}${colors.dim(` and ${files.length - 3} more`)}`;
5796
+ }
5797
+ function logTaxonomyI18nIssues(issues, level) {
5798
+ if (!issues.length)
5799
+ return;
5800
+ const log = level === "error" ? consola.error : consola.warn;
5801
+ const count = level === "error" ? colors.red(String(issues.length)) : colors.yellow(String(issues.length));
5802
+ log(`Detected ${count} taxonomy i18n issue(s).`);
5803
+ for (const issue of issues) {
5804
+ const where = formatFiles(issue.files);
5805
+ if (issue.type === "explicit-locale-key-missing") {
5806
+ log(
5807
+ `[taxonomy-i18n] ${colors.cyan(issue.rawValue)} is an explicit locale key but ${colors.red(`missing in: ${issue.missingLanguages.join(", ")}`)}. Files: ${where}`
5808
+ );
5809
+ continue;
5810
+ }
5811
+ log(
5812
+ `[taxonomy-i18n] ${colors.cyan(issue.rawValue)} resolves to ${colors.cyan(issue.localeKey)} in ${colors.green(issue.presentLanguages.join(", "))}, but is ${colors.red(`missing in: ${issue.missingLanguages.join(", ")}`)}. Files: ${where}`
5813
+ );
5814
+ }
5815
+ }
5816
+ function resolveTaxonomyI18nValidationLevel(options) {
5817
+ return options.config.build.taxonomyI18n?.level || "warn";
5818
+ }
5819
+ async function validateTaxonomyI18n(options) {
5820
+ const level = resolveTaxonomyI18nValidationLevel(options);
5821
+ if (level === "off")
5822
+ return [];
5823
+ const languages = getConfiguredLanguages(options);
5824
+ const [messages, usages] = await Promise.all([
5825
+ loadMergedLocaleMessages(options),
5826
+ collectTaxonomyUsages(options)
5827
+ ]);
5828
+ const issues = findTaxonomyI18nIssues(usages, messages, languages);
5829
+ logTaxonomyI18nIssues(issues, level);
5830
+ if (level === "error" && issues.length)
5831
+ throw new Error(`Taxonomy i18n validation failed with ${issues.length} issue(s).`);
5832
+ return issues;
5833
+ }
5834
+
5653
5835
  function getServerInfoText(msg) {
5654
5836
  return `${valaxyPrefix} ${colors.gray(msg)}`;
5655
5837
  }
@@ -5790,6 +5972,7 @@ async function execBuild({ ssg, ssgEngine, root, output, log }) {
5790
5972
  );
5791
5973
  await callHookWithLog("config:init", valaxyApp);
5792
5974
  await callHookWithLog("build:before", valaxyApp);
5975
+ await validateTaxonomyI18n(options);
5793
5976
  consola.box("\u{1F320} Start building...");
5794
5977
  try {
5795
5978
  if (ssg) {
@@ -6075,6 +6258,7 @@ async function startValaxyDev({
6075
6258
  await valaxyApp.hooks.callHook("content:before-load");
6076
6259
  await loadAllContent(loaders, ctx);
6077
6260
  await valaxyApp.hooks.callHook("content:loaded");
6261
+ await validateTaxonomyI18n(resolvedOptions);
6078
6262
  for (const loader of loaders) {
6079
6263
  if (loader.devPollInterval) {
6080
6264
  const poll = async () => {
@@ -6082,6 +6266,7 @@ async function startValaxyDev({
6082
6266
  await valaxyApp.hooks.callHook("content:before-load");
6083
6267
  await loadAllContent([loader], ctx);
6084
6268
  await valaxyApp.hooks.callHook("content:loaded");
6269
+ await validateTaxonomyI18n(resolvedOptions);
6085
6270
  } catch (error) {
6086
6271
  consola.error("[content-loader] Error while polling:", error);
6087
6272
  } finally {
@@ -6092,6 +6277,8 @@ async function startValaxyDev({
6092
6277
  setTimeout(poll, loader.devPollInterval);
6093
6278
  }
6094
6279
  }
6280
+ } else {
6281
+ await validateTaxonomyI18n(resolvedOptions);
6095
6282
  }
6096
6283
  const viteConfig = mergeConfig({
6097
6284
  // initial vite config
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "valaxy",
3
3
  "type": "module",
4
- "version": "0.28.0-beta.6",
4
+ "version": "0.28.0-beta.7",
5
5
  "description": "📄 Vite & Vue powered static blog generator.",
6
6
  "author": {
7
7
  "email": "me@yunyoujun.cn",
@@ -95,7 +95,7 @@
95
95
  "jiti": "^2.6.1",
96
96
  "js-base64": "^3.7.8",
97
97
  "js-yaml": "^4.1.1",
98
- "katex": "^0.16.38",
98
+ "katex": "^0.16.40",
99
99
  "lru-cache": "^11.2.7",
100
100
  "markdown-it": "^14.1.1",
101
101
  "markdown-it-anchor": "^9.2.0",
@@ -110,7 +110,7 @@
110
110
  "medium-zoom": "^1.1.0",
111
111
  "mermaid": "^11.13.0",
112
112
  "minisearch": "^7.2.0",
113
- "mlly": "^1.8.1",
113
+ "mlly": "^1.8.2",
114
114
  "nprogress": "^0.2.0",
115
115
  "open": "10.1.0",
116
116
  "ora": "^9.3.0",
@@ -129,19 +129,19 @@
129
129
  "unplugin-vue-components": "28.0.0",
130
130
  "unplugin-vue-markdown": "^30.0.0",
131
131
  "vanilla-lazyload": "^19.1.3",
132
- "vite": "^8.0.0",
132
+ "vite": "^8.0.1",
133
133
  "vite-dev-rpc": "^1.1.0",
134
134
  "vite-plugin-vue-devtools": "^8.1.0",
135
- "vite-plugin-vue-layouts-next": "^2.0.1",
135
+ "vite-plugin-vue-layouts-next": "^2.1.0",
136
136
  "vite-ssg": "^28.3.0",
137
137
  "vite-ssg-sitemap": "^0.10.0",
138
138
  "vitepress-plugin-group-icons": "^1.7.1",
139
139
  "vue": "3.5.22",
140
140
  "vue-i18n": "^11.3.0",
141
- "vue-router": "^5.0.3",
141
+ "vue-router": "^5.0.4",
142
142
  "yargs": "^18.0.0",
143
- "@valaxyjs/devtools": "0.28.0-beta.6",
144
- "@valaxyjs/utils": "0.28.0-beta.6"
143
+ "@valaxyjs/utils": "0.28.0-beta.7",
144
+ "@valaxyjs/devtools": "0.28.0-beta.7"
145
145
  },
146
146
  "devDependencies": {
147
147
  "@mdit-vue/plugin-component": "^3.0.2",
@@ -1,6 +1,6 @@
1
1
  import fs from 'fs-extra'
2
2
  import yaml from 'js-yaml'
3
- import { LOCALE_PREFIX } from '../constants'
3
+ import { isLocaleKey, stripLocalePrefix } from '../utils/i18n'
4
4
 
5
5
  export const NODE_I18N: {
6
6
  locales: Record<string, any>
@@ -64,9 +64,9 @@ export function loadLocalesYml(localesPath: string, force = false): Record<strin
64
64
  * ```
65
65
  */
66
66
  export function nodeT(key: string, lang: string): string {
67
- if (key.startsWith(LOCALE_PREFIX)) {
68
- key = key.slice(LOCALE_PREFIX.length)
69
- }
67
+ if (isLocaleKey(key))
68
+ key = stripLocalePrefix(key)
69
+
70
70
  const data = NODE_I18N.locales[lang] || {}
71
71
  const keys = key.split('.')
72
72
  let result: any = data
@@ -1,3 +1,7 @@
1
+ import { LOCALE_PREFIX } from '../constants'
2
+
3
+ export type TaxonomyNamespace = 'tag' | 'category'
4
+
1
5
  /**
2
6
  * translate object
3
7
  *
@@ -15,3 +19,54 @@ export function tObject<T = string>(data: string | Record<string, T>, lang: stri
15
19
  }
16
20
  return data
17
21
  }
22
+
23
+ /**
24
+ * Whether the value is an explicit locale key like `$locale:tag.notes`.
25
+ */
26
+ export function isLocaleKey(value: string): boolean {
27
+ return value.startsWith(LOCALE_PREFIX)
28
+ }
29
+
30
+ /**
31
+ * Strip `$locale:` prefix when present.
32
+ */
33
+ export function stripLocalePrefix(value: string): string {
34
+ return isLocaleKey(value) ? value.slice(LOCALE_PREFIX.length) : value
35
+ }
36
+
37
+ /**
38
+ * Resolve a nested locale message value by dot-separated key.
39
+ */
40
+ export function getLocaleMessageValue(messages: Record<string, any> | undefined, key: string): any {
41
+ return key.split('.').reduce<any>((result, part) => result?.[part], messages)
42
+ }
43
+
44
+ /**
45
+ * Whether a locale message exists for the given key.
46
+ */
47
+ export function hasLocaleMessage(messages: Record<string, any> | undefined, key: string): boolean {
48
+ return getLocaleMessageValue(messages, key) !== undefined
49
+ }
50
+
51
+ /**
52
+ * Resolve the effective locale key for a taxonomy term.
53
+ *
54
+ * - `$locale:tag.notes` -> `tag.notes` (explicit locale key)
55
+ * - `notes` in `tag` namespace -> `tag.notes`
56
+ */
57
+ export function resolveTaxonomyLocaleKey(namespace: TaxonomyNamespace, rawValue: string): {
58
+ localeKey: string
59
+ isExplicitLocaleKey: boolean
60
+ } {
61
+ if (isLocaleKey(rawValue)) {
62
+ return {
63
+ localeKey: stripLocalePrefix(rawValue),
64
+ isExplicitLocaleKey: true,
65
+ }
66
+ }
67
+
68
+ return {
69
+ localeKey: `${namespace}.${rawValue}`,
70
+ isExplicitLocaleKey: false,
71
+ }
72
+ }