valaxy 0.28.0-beta.6 → 0.28.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/client/components/.exclude/ValaxyDebug.vue +176 -0
- package/client/components/ValaxyOpenInEditor.vue +53 -0
- package/client/composables/app/useValaxyApp.ts +1 -7
- package/client/composables/categories.ts +3 -56
- package/client/composables/category-utils.ts +50 -0
- package/client/composables/index.ts +1 -0
- package/client/composables/locale.ts +68 -9
- package/client/composables/outline/anchor.ts +1 -1
- package/client/composables/post/index.ts +6 -3
- package/client/composables/tags.ts +0 -5
- package/client/modules/components.ts +7 -0
- package/client/modules/valaxy.ts +28 -3
- package/dist/node/cli/index.mjs +1 -1
- package/dist/node/index.d.mts +31 -1
- package/dist/node/index.mjs +1 -1
- package/dist/shared/{valaxy.JIuR8V4d.d.mts → valaxy.6MW2qn5T.d.mts} +12 -4
- package/dist/shared/{valaxy.BVsZMcdc.mjs → valaxy.DQ6HsU2J.mjs} +317 -15
- package/dist/types/index.d.mts +2 -2
- package/package.json +11 -12
- package/shared/node/i18n.ts +4 -4
- package/shared/utils/i18n.ts +55 -0
- package/types/config.ts +0 -4
- package/types/frontmatter/page.ts +12 -0
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed, ref } from 'vue'
|
|
3
|
+
import { useRoute } from 'vue-router'
|
|
4
|
+
import { useFrontmatter } from '../../composables/common'
|
|
5
|
+
import { useScreenSize } from '../../composables/helper/useScreenSize'
|
|
6
|
+
import { useLayout } from '../../composables/layout'
|
|
7
|
+
import { useSiteConfig, useThemeConfig } from '../../config'
|
|
8
|
+
import ValaxySvgLogo from '../ValaxySvgLogo.vue'
|
|
9
|
+
|
|
10
|
+
const show = ref(true)
|
|
11
|
+
const expanded = ref<Record<string, boolean>>({
|
|
12
|
+
breakpoints: true,
|
|
13
|
+
route: false,
|
|
14
|
+
frontmatter: false,
|
|
15
|
+
config: false,
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
function toggleSection(key: string) {
|
|
19
|
+
expanded.value[key] = !expanded.value[key]
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Breakpoints
|
|
23
|
+
const screenSize = useScreenSize()
|
|
24
|
+
const breakpoints = computed(() => [
|
|
25
|
+
{ label: 'xs', value: screenSize.isXs.value },
|
|
26
|
+
{ label: 'sm', value: screenSize.isSm.value },
|
|
27
|
+
{ label: 'md', value: screenSize.isMd.value },
|
|
28
|
+
{ label: 'lg', value: screenSize.isLg.value },
|
|
29
|
+
{ label: 'xl', value: screenSize.isXl.value },
|
|
30
|
+
{ label: '2xl', value: screenSize.is2xl.value },
|
|
31
|
+
])
|
|
32
|
+
|
|
33
|
+
// Route
|
|
34
|
+
const route = useRoute()
|
|
35
|
+
const routeInfo = computed(() => ({
|
|
36
|
+
path: route.path,
|
|
37
|
+
name: route.name as string,
|
|
38
|
+
layout: route.meta?.layout || 'default',
|
|
39
|
+
query: Object.keys(route.query).length ? route.query : undefined,
|
|
40
|
+
params: Object.keys(route.params).length ? route.params : undefined,
|
|
41
|
+
}))
|
|
42
|
+
|
|
43
|
+
// Frontmatter
|
|
44
|
+
const frontmatter = useFrontmatter()
|
|
45
|
+
const fmSummary = computed(() => {
|
|
46
|
+
const fm = frontmatter.value
|
|
47
|
+
if (!fm || !Object.keys(fm).length)
|
|
48
|
+
return null
|
|
49
|
+
return fm
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
// Config
|
|
53
|
+
const siteConfig = useSiteConfig()
|
|
54
|
+
const themeConfig = useThemeConfig()
|
|
55
|
+
const layout = useLayout()
|
|
56
|
+
|
|
57
|
+
const configSummary = computed(() => ({
|
|
58
|
+
theme: siteConfig.value.lang ? undefined : undefined,
|
|
59
|
+
lang: siteConfig.value.lang,
|
|
60
|
+
title: siteConfig.value.title,
|
|
61
|
+
url: siteConfig.value.url,
|
|
62
|
+
layout: layout.value,
|
|
63
|
+
}))
|
|
64
|
+
</script>
|
|
65
|
+
|
|
66
|
+
<template>
|
|
67
|
+
<div
|
|
68
|
+
v-if="show"
|
|
69
|
+
class="valaxy-debug fixed bottom-4 left-2 z-9999 max-h-[80vh] w-72 overflow-y-auto rounded-lg bg-black/80 p-3 text-xs text-white shadow-lg backdrop-blur-sm"
|
|
70
|
+
@click.stop
|
|
71
|
+
>
|
|
72
|
+
<!-- Header -->
|
|
73
|
+
<div class="mb-2 flex items-center justify-between border-b border-white/20 pb-2">
|
|
74
|
+
<span class="flex items-center gap-1 font-bold text-cyan-400">
|
|
75
|
+
<ValaxySvgLogo class="size-4" />
|
|
76
|
+
Valaxy Debug
|
|
77
|
+
</span>
|
|
78
|
+
<button
|
|
79
|
+
class="rounded px-1 text-white/60 transition hover:bg-white/20 hover:text-white"
|
|
80
|
+
title="Close"
|
|
81
|
+
@click="show = false"
|
|
82
|
+
>
|
|
83
|
+
✕
|
|
84
|
+
</button>
|
|
85
|
+
</div>
|
|
86
|
+
|
|
87
|
+
<!-- Breakpoints -->
|
|
88
|
+
<div class="mb-1">
|
|
89
|
+
<button
|
|
90
|
+
class="w-full rounded px-1 py-0.5 text-left font-bold text-emerald-400 transition hover:bg-white/10"
|
|
91
|
+
@click="toggleSection('breakpoints')"
|
|
92
|
+
>
|
|
93
|
+
{{ expanded.breakpoints ? '▾' : '▸' }} Breakpoints
|
|
94
|
+
</button>
|
|
95
|
+
<div v-if="expanded.breakpoints" class="mt-1 flex flex-wrap gap-1 pl-3">
|
|
96
|
+
<span
|
|
97
|
+
v-for="bp in breakpoints"
|
|
98
|
+
:key="bp.label"
|
|
99
|
+
class="rounded px-1.5 py-0.5"
|
|
100
|
+
:class="bp.value ? 'bg-emerald-500/30 text-emerald-300' : 'bg-white/5 text-white/40'"
|
|
101
|
+
>
|
|
102
|
+
{{ bp.label }}
|
|
103
|
+
</span>
|
|
104
|
+
</div>
|
|
105
|
+
</div>
|
|
106
|
+
|
|
107
|
+
<!-- Route -->
|
|
108
|
+
<div class="mb-1">
|
|
109
|
+
<button
|
|
110
|
+
class="w-full rounded px-1 py-0.5 text-left font-bold text-blue-400 transition hover:bg-white/10"
|
|
111
|
+
@click="toggleSection('route')"
|
|
112
|
+
>
|
|
113
|
+
{{ expanded.route ? '▾' : '▸' }} Route
|
|
114
|
+
</button>
|
|
115
|
+
<div v-if="expanded.route" class="mt-1 space-y-0.5 pl-3">
|
|
116
|
+
<div><span class="text-white/50">path:</span> {{ routeInfo.path }}</div>
|
|
117
|
+
<div><span class="text-white/50">name:</span> {{ routeInfo.name }}</div>
|
|
118
|
+
<div><span class="text-white/50">layout:</span> {{ routeInfo.layout }}</div>
|
|
119
|
+
<div v-if="routeInfo.query">
|
|
120
|
+
<span class="text-white/50">query:</span> {{ JSON.stringify(routeInfo.query) }}
|
|
121
|
+
</div>
|
|
122
|
+
<div v-if="routeInfo.params">
|
|
123
|
+
<span class="text-white/50">params:</span> {{ JSON.stringify(routeInfo.params) }}
|
|
124
|
+
</div>
|
|
125
|
+
</div>
|
|
126
|
+
</div>
|
|
127
|
+
|
|
128
|
+
<!-- Frontmatter -->
|
|
129
|
+
<div class="mb-1">
|
|
130
|
+
<button
|
|
131
|
+
class="w-full rounded px-1 py-0.5 text-left font-bold text-amber-400 transition hover:bg-white/10"
|
|
132
|
+
@click="toggleSection('frontmatter')"
|
|
133
|
+
>
|
|
134
|
+
{{ expanded.frontmatter ? '▾' : '▸' }} Frontmatter
|
|
135
|
+
</button>
|
|
136
|
+
<div v-if="expanded.frontmatter" class="mt-1 pl-3">
|
|
137
|
+
<pre v-if="fmSummary" class="max-h-48 overflow-auto whitespace-pre-wrap break-all rounded bg-white/5 p-1.5 text-white/80">{{ JSON.stringify(fmSummary, null, 2) }}</pre>
|
|
138
|
+
<span v-else class="text-white/40">No frontmatter</span>
|
|
139
|
+
</div>
|
|
140
|
+
</div>
|
|
141
|
+
|
|
142
|
+
<!-- Config -->
|
|
143
|
+
<div class="mb-1">
|
|
144
|
+
<button
|
|
145
|
+
class="w-full rounded px-1 py-0.5 text-left font-bold text-purple-400 transition hover:bg-white/10"
|
|
146
|
+
@click="toggleSection('config')"
|
|
147
|
+
>
|
|
148
|
+
{{ expanded.config ? '▾' : '▸' }} Config
|
|
149
|
+
</button>
|
|
150
|
+
<div v-if="expanded.config" class="mt-1 space-y-2 pl-3">
|
|
151
|
+
<div>
|
|
152
|
+
<div class="mb-0.5 text-white/50">
|
|
153
|
+
Site Config
|
|
154
|
+
</div>
|
|
155
|
+
<pre class="max-h-48 overflow-auto whitespace-pre-wrap break-all rounded bg-white/5 p-1.5 text-white/80">{{ JSON.stringify(configSummary, null, 2) }}</pre>
|
|
156
|
+
</div>
|
|
157
|
+
<div>
|
|
158
|
+
<div class="mb-0.5 text-white/50">
|
|
159
|
+
Theme Config
|
|
160
|
+
</div>
|
|
161
|
+
<pre class="max-h-48 overflow-auto whitespace-pre-wrap break-all rounded bg-white/5 p-1.5 text-white/80">{{ JSON.stringify(themeConfig, null, 2) }}</pre>
|
|
162
|
+
</div>
|
|
163
|
+
</div>
|
|
164
|
+
</div>
|
|
165
|
+
</div>
|
|
166
|
+
|
|
167
|
+
<!-- Collapsed toggle button -->
|
|
168
|
+
<button
|
|
169
|
+
v-if="!show"
|
|
170
|
+
class="fixed bottom-4 left-2 z-9999 rounded-lg bg-black/60 px-2 py-1 text-xs text-white/60 shadow-lg backdrop-blur-sm transition hover:bg-black/80 hover:text-white"
|
|
171
|
+
title="Open Valaxy Debug"
|
|
172
|
+
@click="show = true"
|
|
173
|
+
>
|
|
174
|
+
<ValaxySvgLogo class="size-4" />
|
|
175
|
+
</button>
|
|
176
|
+
</template>
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
<script lang="ts" setup>
|
|
2
|
+
import { useData } from '../composables'
|
|
3
|
+
|
|
4
|
+
const { page } = useData()
|
|
5
|
+
|
|
6
|
+
const isDev = import.meta.env.DEV
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Open the current page source file in editor (dev mode only)
|
|
10
|
+
* Uses Vite's built-in `/__open-in-editor` endpoint
|
|
11
|
+
*/
|
|
12
|
+
function openInEditor() {
|
|
13
|
+
const filePath = page.value?.filePath
|
|
14
|
+
if (filePath) {
|
|
15
|
+
fetch(`${window.location.origin}/__open-in-editor?file=${encodeURIComponent(filePath)}`)
|
|
16
|
+
.catch((err) => {
|
|
17
|
+
console.error('[valaxy] Failed to open in editor:', err)
|
|
18
|
+
})
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
</script>
|
|
22
|
+
|
|
23
|
+
<template>
|
|
24
|
+
<button
|
|
25
|
+
v-if="isDev"
|
|
26
|
+
class="valaxy-open-in-editor"
|
|
27
|
+
title="Open in Editor"
|
|
28
|
+
@click="openInEditor"
|
|
29
|
+
>
|
|
30
|
+
<slot>
|
|
31
|
+
<div i-ri-code-s-slash-line />
|
|
32
|
+
</slot>
|
|
33
|
+
</button>
|
|
34
|
+
</template>
|
|
35
|
+
|
|
36
|
+
<style>
|
|
37
|
+
.valaxy-open-in-editor {
|
|
38
|
+
display: inline-flex;
|
|
39
|
+
align-items: center;
|
|
40
|
+
cursor: pointer;
|
|
41
|
+
opacity: 0.4;
|
|
42
|
+
transition: opacity 0.2s;
|
|
43
|
+
background: none;
|
|
44
|
+
border: none;
|
|
45
|
+
padding: 0;
|
|
46
|
+
color: inherit;
|
|
47
|
+
font-size: inherit;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.valaxy-open-in-editor:hover {
|
|
51
|
+
opacity: 1;
|
|
52
|
+
}
|
|
53
|
+
</style>
|
|
@@ -1,12 +1,8 @@
|
|
|
1
1
|
import { definePerson, defineWebPage, defineWebSite, useSchemaOrg } from '@unhead/schema-org/vue'
|
|
2
2
|
import { useSeoMeta } from '@unhead/vue'
|
|
3
|
-
|
|
4
|
-
// TODO: add docs to override ValaxyApp
|
|
5
3
|
import { computed } from 'vue'
|
|
6
4
|
import { useI18n } from 'vue-i18n'
|
|
7
|
-
|
|
8
5
|
import { useRoute } from 'vue-router'
|
|
9
|
-
|
|
10
6
|
import { useFrontmatter, useLocale, useValaxyHead, useValaxyI18n } from '../../composables'
|
|
11
7
|
import { useTimezone } from '../../composables/global'
|
|
12
8
|
// https://github.com/vueuse/head
|
|
@@ -16,7 +12,6 @@ import { useSiteConfig } from '../../config'
|
|
|
16
12
|
|
|
17
13
|
export function useValaxyApp() {
|
|
18
14
|
const siteConfig = useSiteConfig()
|
|
19
|
-
// todo, allow user config
|
|
20
15
|
const fm = useFrontmatter()
|
|
21
16
|
|
|
22
17
|
const { locale } = useI18n()
|
|
@@ -32,7 +27,6 @@ export function useValaxyApp() {
|
|
|
32
27
|
}
|
|
33
28
|
|
|
34
29
|
// seo
|
|
35
|
-
// todo: get first image url from markdown
|
|
36
30
|
const siteUrl = computed(() => fm.value.url || siteConfig.value.url)
|
|
37
31
|
const description = computed(() => $tO(fm.value.excerpt) || $tO(fm.value.description) || $t(siteConfig.value.description))
|
|
38
32
|
|
|
@@ -43,7 +37,7 @@ export function useValaxyApp() {
|
|
|
43
37
|
ogLocaleAlternate: computed(() => siteConfig.value.languages.filter(l => l !== locale.value)),
|
|
44
38
|
ogSiteName: computed(() => $t(siteConfig.value.title)),
|
|
45
39
|
ogTitle: computed(() => $tO(fm.value.title) || $t(siteConfig.value.title)),
|
|
46
|
-
ogImage: computed(() => fm.value.ogImage || fm.value.cover || siteConfig.value.favicon),
|
|
40
|
+
ogImage: computed(() => fm.value.ogImage || fm.value.cover || fm.value.firstImage || siteConfig.value.favicon),
|
|
47
41
|
ogType: 'website',
|
|
48
42
|
ogUrl: siteUrl,
|
|
49
43
|
})
|
|
@@ -1,47 +1,11 @@
|
|
|
1
1
|
import type { MaybeRef } from 'vue'
|
|
2
2
|
import type { Post } from '../../types'
|
|
3
|
+
import type { CategoryList } from './category-utils'
|
|
3
4
|
import { computed, unref } from 'vue'
|
|
4
5
|
import { useSiteStore } from '../stores'
|
|
5
6
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
*/
|
|
9
|
-
export interface BaseCategory {
|
|
10
|
-
/**
|
|
11
|
-
* 分类下的文章数量
|
|
12
|
-
*/
|
|
13
|
-
total: number
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* @en
|
|
18
|
-
* Category list
|
|
19
|
-
*
|
|
20
|
-
* @zh
|
|
21
|
-
* 分类列表
|
|
22
|
-
*/
|
|
23
|
-
export interface CategoryList {
|
|
24
|
-
/**
|
|
25
|
-
* category name
|
|
26
|
-
*/
|
|
27
|
-
name: string
|
|
28
|
-
/**
|
|
29
|
-
* total posts
|
|
30
|
-
*/
|
|
31
|
-
total: number
|
|
32
|
-
children: Map<string, Post | CategoryList>
|
|
33
|
-
}
|
|
34
|
-
export type Category = CategoryList
|
|
35
|
-
export type Categories = Map<string, Post | CategoryList>
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* For theme development, you can use this function to determine whether the category is a category list.
|
|
39
|
-
* @todo write unit test
|
|
40
|
-
* @param category
|
|
41
|
-
*/
|
|
42
|
-
export function isCategoryList(category: any): category is CategoryList {
|
|
43
|
-
return category.children
|
|
44
|
-
}
|
|
7
|
+
export type { BaseCategory, Categories, Category, CategoryList } from './category-utils'
|
|
8
|
+
export { isCategoryList, removeItemFromCategory } from './category-utils'
|
|
45
9
|
|
|
46
10
|
/**
|
|
47
11
|
* get categories from posts
|
|
@@ -158,20 +122,3 @@ export function useCategories(category?: MaybeRef<string>, posts: Post[] = []) {
|
|
|
158
122
|
}
|
|
159
123
|
})
|
|
160
124
|
}
|
|
161
|
-
|
|
162
|
-
/**
|
|
163
|
-
* remove item from category
|
|
164
|
-
* @param categoryList
|
|
165
|
-
* @param categoryName
|
|
166
|
-
*/
|
|
167
|
-
export function removeItemFromCategory(categoryList: CategoryList, categoryName: string) {
|
|
168
|
-
if (isCategoryList(categoryList)) {
|
|
169
|
-
const categoryArr = categoryName.split('/')
|
|
170
|
-
categoryList.children.delete(categoryArr[0])
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
/**
|
|
175
|
-
* @deprecated use `useCategories` instead
|
|
176
|
-
*/
|
|
177
|
-
export const useCategory = useCategories
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { Post } from '../../types'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 基础分类
|
|
5
|
+
*/
|
|
6
|
+
export interface BaseCategory {
|
|
7
|
+
/**
|
|
8
|
+
* 分类下的文章数量
|
|
9
|
+
*/
|
|
10
|
+
total: number
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @en
|
|
15
|
+
* Category list
|
|
16
|
+
*
|
|
17
|
+
* @zh
|
|
18
|
+
* 分类列表
|
|
19
|
+
*/
|
|
20
|
+
export interface CategoryList {
|
|
21
|
+
/**
|
|
22
|
+
* category name
|
|
23
|
+
*/
|
|
24
|
+
name: string
|
|
25
|
+
/**
|
|
26
|
+
* total posts
|
|
27
|
+
*/
|
|
28
|
+
total: number
|
|
29
|
+
children: Map<string, Post | CategoryList>
|
|
30
|
+
}
|
|
31
|
+
export type Category = CategoryList
|
|
32
|
+
export type Categories = Map<string, Post | CategoryList>
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* For theme development, you can use this function to determine whether the category is a category list.
|
|
36
|
+
* @param category
|
|
37
|
+
*/
|
|
38
|
+
export function isCategoryList(category: unknown): category is CategoryList {
|
|
39
|
+
return !!category && typeof category === 'object' && 'children' in category && category.children instanceof Map
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* remove item from category
|
|
44
|
+
* @param categoryList
|
|
45
|
+
* @param categoryName
|
|
46
|
+
*/
|
|
47
|
+
export function removeItemFromCategory(categoryList: CategoryList, categoryName: string) {
|
|
48
|
+
const categoryArr = categoryName.split('/')
|
|
49
|
+
categoryList.children.delete(categoryArr[0])
|
|
50
|
+
}
|
|
@@ -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
|
-
|
|
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
|
|
90
|
-
return t(key
|
|
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
|
-
|
|
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,5 +1,6 @@
|
|
|
1
1
|
import type { ValaxySSGContext } from '../setups'
|
|
2
2
|
|
|
3
|
+
import { defineAsyncComponent } from 'vue'
|
|
3
4
|
import AppLink from '../components/AppLink.vue'
|
|
4
5
|
import ValaxyTranslate from '../components/builtin/ValaxyTranslate.vue'
|
|
5
6
|
|
|
@@ -10,4 +11,10 @@ import ValaxyTranslate from '../components/builtin/ValaxyTranslate.vue'
|
|
|
10
11
|
export function registerGlobalComponents(ctx: ValaxySSGContext) {
|
|
11
12
|
ctx.app.component('AppLink', AppLink)
|
|
12
13
|
ctx.app.component('VT', ValaxyTranslate)
|
|
14
|
+
|
|
15
|
+
// DEV-only: register ValaxyDebug component (tree-shaken in production)
|
|
16
|
+
if (import.meta.env.DEV) {
|
|
17
|
+
const ValaxyDebug = defineAsyncComponent(() => import('../components/.exclude/ValaxyDebug.vue'))
|
|
18
|
+
ctx.app.component('ValaxyDebug', ValaxyDebug)
|
|
19
|
+
}
|
|
13
20
|
}
|
package/client/modules/valaxy.ts
CHANGED
|
@@ -14,6 +14,7 @@ import type { PageDataPayload } from '../../types'
|
|
|
14
14
|
import type { ValaxySSGContext } from '../setups'
|
|
15
15
|
import { ensureSuffix } from '@antfu/utils'
|
|
16
16
|
import { useStorage } from '@vueuse/core'
|
|
17
|
+
import { nextTick, watch } from 'vue'
|
|
17
18
|
import { createI18n } from 'vue-i18n'
|
|
18
19
|
|
|
19
20
|
// @ts-expect-error virtual
|
|
@@ -52,11 +53,35 @@ export const i18n = createI18n({
|
|
|
52
53
|
})
|
|
53
54
|
|
|
54
55
|
export async function install({ app, router }: ValaxySSGContext, config: ComputedRef<ValaxyConfig<DefaultTheme.Config>>) {
|
|
55
|
-
const
|
|
56
|
-
|
|
56
|
+
const defaultLang = config?.value.siteConfig.lang || 'en'
|
|
57
|
+
|
|
58
|
+
// During SSR/SSG build **and** the initial client hydration pass we must
|
|
59
|
+
// keep the locale at `defaultLang` so that the rendered HTML matches on
|
|
60
|
+
// both sides — no hydration mismatch for any i18n-dependent attribute
|
|
61
|
+
// (title, class, text content, etc.).
|
|
62
|
+
//
|
|
63
|
+
// The stored user preference is restored **after** hydration is complete
|
|
64
|
+
// (router.isReady + nextTick) so Vue can patch the DOM normally.
|
|
65
|
+
i18n.global.locale.value = defaultLang
|
|
57
66
|
|
|
58
67
|
app.use(i18n)
|
|
59
|
-
|
|
68
|
+
|
|
69
|
+
router.isReady().then(async () => {
|
|
70
|
+
// Wait for the hydration to finish before restoring the stored locale.
|
|
71
|
+
await nextTick()
|
|
72
|
+
|
|
73
|
+
const storedLocale = useStorage('valaxy-locale', defaultLang)
|
|
74
|
+
|
|
75
|
+
// Apply the stored locale (if different from default)
|
|
76
|
+
if (storedLocale.value && storedLocale.value !== i18n.global.locale.value)
|
|
77
|
+
i18n.global.locale.value = storedLocale.value
|
|
78
|
+
|
|
79
|
+
// Keep i18n locale in sync when the stored value changes later
|
|
80
|
+
watch(storedLocale, (val) => {
|
|
81
|
+
if (val)
|
|
82
|
+
i18n.global.locale.value = val
|
|
83
|
+
})
|
|
84
|
+
|
|
60
85
|
handleHMR(router)
|
|
61
86
|
})
|
|
62
87
|
}
|
package/dist/node/cli/index.mjs
CHANGED
|
@@ -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.
|
|
4
|
+
export { c as cli, I as registerDevCommand, W as run, Z as startValaxyDev } from '../../shared/valaxy.DQ6HsU2J.mjs';
|
|
5
5
|
import 'node:os';
|
|
6
6
|
import 'node:path';
|
|
7
7
|
import 'consola';
|