Skip to content

Commit e31551f

Browse files
committed
feat: add i18n support with language switcher (zh/en) for all pages
1 parent 90a5de5 commit e31551f

30 files changed

Lines changed: 1079 additions & 457 deletions

package-lock.json

Lines changed: 65 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,15 @@
1010
},
1111
"dependencies": {
1212
"vue": "^3.5.13",
13+
"vue-i18n": "^9.14.4",
1314
"vue-router": "^4.5.0"
1415
},
1516
"devDependencies": {
17+
"@tailwindcss/typography": "^0.5.16",
1618
"@vitejs/plugin-vue": "^5.2.3",
1719
"autoprefixer": "^10.4.21",
1820
"postcss": "^8.5.3",
1921
"tailwindcss": "^3.4.17",
20-
"@tailwindcss/typography": "^0.5.16",
2122
"typescript": "~5.7.3",
2223
"vite": "^6.3.2",
2324
"vue-tsc": "^2.2.8"

src/components/DocFooter.vue

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
<script setup lang="ts">
22
import { computed } from 'vue'
33
import { useRoute } from 'vue-router'
4+
import { useI18n } from 'vue-i18n'
45
import { getPrevNext, llmSimpleRouterSidebar } from '../config/sidebar'
56
67
const route = useRoute()
8+
const { t } = useI18n()
79
const navigation = computed(() => getPrevNext(llmSimpleRouterSidebar, route.path))
810
</script>
911

@@ -14,9 +16,9 @@ const navigation = computed(() => getPrevNext(llmSimpleRouterSidebar, route.path
1416
:to="navigation.prev.path"
1517
class="group flex flex-col items-start rounded-lg border border-white/10 px-4 py-3 transition-colors hover:border-white/20 hover:bg-white/5"
1618
>
17-
<span class="text-xs text-gray-500">上一页</span>
19+
<span class="text-xs text-gray-500">{{ t('footer.prev') }}</span>
1820
<span class="text-sm text-gray-300 group-hover:text-white transition-colors">
19-
{{ navigation.prev.title }}
21+
{{ t(navigation.prev.titleKey) }}
2022
</span>
2123
</router-link>
2224
<div v-else />
@@ -26,9 +28,9 @@ const navigation = computed(() => getPrevNext(llmSimpleRouterSidebar, route.path
2628
:to="navigation.next.path"
2729
class="group flex flex-col items-end rounded-lg border border-white/10 px-4 py-3 transition-colors hover:border-white/20 hover:bg-white/5"
2830
>
29-
<span class="text-xs text-gray-500">下一页</span>
31+
<span class="text-xs text-gray-500">{{ t('footer.next') }}</span>
3032
<span class="text-sm text-gray-300 group-hover:text-white transition-colors">
31-
{{ navigation.next.title }}
33+
{{ t(navigation.next.titleKey) }}
3234
</span>
3335
</router-link>
3436
<div v-else />

src/components/NavBar.vue

Lines changed: 30 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,26 @@
11
<script setup lang="ts">
2-
import { ref } from 'vue'
2+
import { ref, watch } from 'vue'
33
import { useRoute } from 'vue-router'
4+
import { useI18n } from 'vue-i18n'
45
56
const route = useRoute()
7+
const { t, locale } = useI18n()
68
const mobileOpen = ref(false)
79
const projectDropdownOpen = ref(false)
810
911
const projects = [
1012
{ name: 'LLM Simple Router', path: '/project/llm-simple-router/' },
1113
]
1214
13-
function toggleMobile() {
14-
mobileOpen.value = !mobileOpen.value
15-
}
15+
watch(locale, (val) => { localStorage.setItem('locale', val) })
1616
17-
function isActive(path: string): boolean {
18-
return route.path.startsWith(path)
19-
}
17+
function toggleMobile() { mobileOpen.value = !mobileOpen.value }
18+
function isActive(path: string): boolean { return route.path.startsWith(path) }
19+
function toggleLocale() { locale.value = locale.value === 'zh' ? 'en' : 'zh' }
2020
</script>
2121

2222
<template>
2323
<nav class="fixed top-0 left-0 right-0 z-50 flex h-14 items-center border-b border-white/10 bg-surface/80 backdrop-blur-md">
24-
<!-- 左侧 Logo 区域:w-64 与 SideBar 对齐 -->
2524
<router-link to="/" class="flex h-14 w-64 shrink-0 items-center px-6 group">
2625
<svg width="134" height="30" viewBox="0 0 134 30" fill="none" class="transition-opacity group-hover:opacity-80">
2726
<text x="0" y="24" font-family="monospace" font-size="20" font-weight="bold" fill="#60a5fa">Z</text>
@@ -38,11 +37,10 @@ function isActive(path: string): boolean {
3837
</svg>
3938
</router-link>
4039

41-
<!-- 右侧:导航菜单 + 图标(从 SideBar 分割线位置开始) -->
4240
<div class="hidden flex-1 items-center justify-between pr-6 md:flex">
4341
<div class="flex items-center gap-1">
4442
<router-link to="/about" class="rounded-md px-3 py-1.5 text-sm text-gray-400 hover:bg-white/5 hover:text-gray-200 transition-colors">
45-
个人介绍
43+
{{ t('nav.about') }}
4644
</router-link>
4745

4846
<div class="relative" @mouseleave="projectDropdownOpen = false">
@@ -51,33 +49,31 @@ function isActive(path: string): boolean {
5149
:class="isActive('/project') ? 'text-gray-200 bg-white/5' : 'text-gray-400 hover:bg-white/5 hover:text-gray-200'"
5250
@mouseenter="projectDropdownOpen = true"
5351
>
54-
项目介绍
55-
<svg class="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
56-
<path stroke-linecap="round" stroke-linejoin="round" d="M19 9l-7 7-7-7" />
57-
</svg>
52+
{{ t('nav.projects') }}
53+
<svg class="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M19 9l-7 7-7-7" /></svg>
5854
</button>
5955
<div v-show="projectDropdownOpen" class="absolute left-0 top-[calc(100%-4px)] z-10" @mouseenter="projectDropdownOpen = true">
6056
<div class="h-1 w-full" />
6157
<div class="w-56 rounded-lg border border-white/10 bg-surface-50 py-1 shadow-xl">
62-
<router-link
63-
v-for="project in projects"
64-
:key="project.path"
65-
:to="project.path"
66-
class="block px-4 py-2 text-sm text-gray-300 hover:bg-white/5 hover:text-white transition-colors"
67-
@click="projectDropdownOpen = false"
68-
>
69-
{{ project.name }}
70-
</router-link>
58+
<router-link v-for="p in projects" :key="p.path" :to="p.path" class="block px-4 py-2 text-sm text-gray-300 hover:bg-white/5 hover:text-white transition-colors" @click="projectDropdownOpen = false">{{ p.name }}</router-link>
7159
</div>
7260
</div>
7361
</div>
7462

7563
<router-link to="/social" class="rounded-md px-3 py-1.5 text-sm text-gray-400 hover:bg-white/5 hover:text-gray-200 transition-colors">
76-
社交平台
64+
{{ t('nav.social') }}
7765
</router-link>
7866
</div>
7967

80-
<div class="flex items-center gap-3">
68+
<div class="flex items-center gap-2">
69+
<!-- Language switcher -->
70+
<button @click="toggleLocale" class="flex items-center gap-1 rounded-md px-2 py-1 text-xs font-medium text-gray-400 hover:text-gray-200 hover:bg-white/5 transition-colors" title="Switch Language">
71+
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
72+
<path stroke-linecap="round" stroke-linejoin="round" d="M3 5h12M9 3v2m1.048 9.5A18.022 18.022 0 016.412 9m6.088 9h7M11 21l5-10 5 10M12.751 5C11.783 10.77 8.07 15.61 3 18.129" />
73+
</svg>
74+
<span>{{ locale === 'zh' ? 'EN' : '中文' }}</span>
75+
</button>
76+
8177
<a href="https://github.com/zhushanwen321" target="_blank" rel="noopener" class="text-gray-500 hover:text-gray-300 transition-colors" aria-label="GitHub">
8278
<svg class="h-5 w-5" fill="currentColor" viewBox="0 0 24 24"><path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/></svg>
8379
</a>
@@ -88,22 +84,25 @@ function isActive(path: string): boolean {
8884
</div>
8985

9086
<!-- Mobile hamburger -->
91-
<div class="flex flex-1 items-center justify-end pr-6 md:hidden">
87+
<div class="flex flex-1 items-center justify-end gap-2 pr-4 md:hidden">
88+
<button @click="toggleLocale" class="text-xs text-gray-400 hover:text-gray-200 px-1.5 py-1">
89+
{{ locale === 'zh' ? 'EN' : '中文' }}
90+
</button>
9291
<button class="text-gray-400 hover:text-gray-200 transition-colors" @click="toggleMobile" aria-label="Toggle menu">
9392
<svg v-if="!mobileOpen" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M4 6h16M4 12h16M4 18h16" /></svg>
9493
<svg v-else class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" /></svg>
9594
</button>
9695
</div>
9796

9897
<!-- Mobile menu -->
99-
<div v-show="mobileOpen" class="absolute left-0 right-0 top-14 border-t border-white/10 md:hidden">
98+
<div v-show="mobileOpen" class="absolute left-0 right-0 top-14 border-t border-white/10 md:hidden bg-surface">
10099
<div class="space-y-1 px-4 py-3">
101-
<router-link to="/about" class="block rounded-md px-3 py-2 text-sm text-gray-300 hover:bg-white/5" @click="mobileOpen = false">个人介绍</router-link>
100+
<router-link to="/about" class="block rounded-md px-3 py-2 text-sm text-gray-300 hover:bg-white/5" @click="mobileOpen = false">{{ t('nav.about') }}</router-link>
102101
<div class="pl-3">
103-
<p class="px-3 py-1 text-xs font-semibold uppercase tracking-wider text-gray-500">项目介绍</p>
104-
<router-link v-for="project in projects" :key="project.path" :to="project.path" class="block rounded-md px-3 py-2 text-sm text-gray-300 hover:bg-white/5" @click="mobileOpen = false">{{ project.name }}</router-link>
102+
<p class="px-3 py-1 text-xs font-semibold uppercase tracking-wider text-gray-500">{{ t('nav.projects') }}</p>
103+
<router-link v-for="p in projects" :key="p.path" :to="p.path" class="block rounded-md px-3 py-2 text-sm text-gray-300 hover:bg-white/5" @click="mobileOpen = false">{{ p.name }}</router-link>
105104
</div>
106-
<router-link to="/social" class="block rounded-md px-3 py-2 text-sm text-gray-300 hover:bg-white/5" @click="mobileOpen = false">社交平台</router-link>
105+
<router-link to="/social" class="block rounded-md px-3 py-2 text-sm text-gray-300 hover:bg-white/5" @click="mobileOpen = false">{{ t('nav.social') }}</router-link>
107106
</div>
108107
</div>
109108
</nav>

src/components/SideBar.vue

Lines changed: 12 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,28 @@
11
<script setup lang="ts">
22
import { ref, watch } from 'vue'
33
import { useRoute } from 'vue-router'
4+
import { useI18n } from 'vue-i18n'
45
import { llmSimpleRouterSidebar } from '../config/sidebar'
56
67
const route = useRoute()
8+
const { t } = useI18n()
79
const sidebar = llmSimpleRouterSidebar
810
const expandedGroups = ref<Record<string, boolean>>({})
911
10-
// 自动展开包含当前路由的分组
1112
watch(
1213
() => route.path,
13-
(_path) => {
14+
() => {
1415
for (const group of sidebar) {
1516
if (group.items.some((item) => isActive(item.path))) {
16-
expandedGroups.value[group.title] = true
17+
expandedGroups.value[group.titleKey] = true
1718
}
1819
}
1920
},
2021
{ immediate: true },
2122
)
2223
23-
function toggleGroup(title: string) {
24-
expandedGroups.value[title] = !expandedGroups.value[title]
24+
function toggleGroup(titleKey: string) {
25+
expandedGroups.value[titleKey] = !expandedGroups.value[titleKey]
2526
}
2627
2728
function isActive(path: string): boolean {
@@ -35,25 +36,21 @@ function isActive(path: string): boolean {
3536
<template>
3637
<aside class="fixed left-0 top-14 z-40 h-[calc(100vh-3.5rem)] w-64 border-r border-white/10 bg-surface-50 overflow-y-auto">
3738
<nav class="p-4">
38-
<div
39-
v-for="group in sidebar"
40-
:key="group.title"
41-
class="mb-3"
42-
>
39+
<div v-for="group in sidebar" :key="group.titleKey" class="mb-3">
4340
<button
4441
class="flex w-full items-center justify-between rounded-md px-2 py-1.5 text-xs font-semibold uppercase tracking-wider text-gray-500 hover:text-gray-300 transition-colors"
45-
@click="toggleGroup(group.title)"
42+
@click="toggleGroup(group.titleKey)"
4643
>
47-
{{ group.title }}
44+
{{ t(group.titleKey) }}
4845
<svg
4946
class="h-3.5 w-3.5 transition-transform"
50-
:class="{ 'rotate-90': !expandedGroups[group.title] }"
47+
:class="{ 'rotate-90': !expandedGroups[group.titleKey] }"
5148
fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"
5249
>
5350
<path stroke-linecap="round" stroke-linejoin="round" d="M9 5l7 7-7 7" />
5451
</svg>
5552
</button>
56-
<div v-show="expandedGroups[group.title] !== false">
53+
<div v-show="expandedGroups[group.titleKey] !== false">
5754
<router-link
5855
v-for="item in group.items"
5956
:key="item.path"
@@ -67,7 +64,7 @@ function isActive(path: string): boolean {
6764
class="absolute left-0 top-1/2 -translate-y-1/2 w-0.5 h-4 rounded-r transition-colors"
6865
:class="isActive(item.path) ? 'bg-accent' : 'bg-transparent'"
6966
/>
70-
{{ item.title }}
67+
{{ t(item.titleKey) }}
7168
</router-link>
7269
</div>
7370
</div>

src/composables/useLocale.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { useI18n } from 'vue-i18n'
2+
import { computed } from 'vue'
3+
4+
/** 可用的语言列表 */
5+
export const availableLocales = [
6+
{ code: 'zh', label: '中文' },
7+
{ code: 'en', label: 'English' },
8+
] as const
9+
10+
export type LocaleCode = (typeof availableLocales)[number]['code']
11+
12+
/** 获取当前语言 */
13+
export function useLocale() {
14+
const { locale, t } = useI18n()
15+
const current = computed(() => availableLocales.find((l) => l.code === locale.value)!)
16+
const other = computed(() => availableLocales.find((l) => l.code !== locale.value)!)
17+
18+
function switchLocale() {
19+
locale.value = other.value.code
20+
}
21+
22+
return { locale, current, other, switchLocale, t }
23+
}
24+
25+
/** SideBar keys 常量 */
26+
export const SIDEBAR_KEYS = {
27+
quickStart: 'sidebar.quickStart',
28+
overview: 'sidebar.overview',
29+
startRouter: 'sidebar.startRouter',
30+
features: 'sidebar.features',
31+
autoRetry: 'sidebar.autoRetry',
32+
providers: 'sidebar.providers',
33+
modelMapping: 'sidebar.modelMapping',
34+
concurrency: 'sidebar.concurrency',
35+
failover: 'sidebar.failover',
36+
monitor: 'sidebar.monitor',
37+
multiKey: 'sidebar.multiKey',
38+
config: 'sidebar.config',
39+
claudeCode: 'sidebar.claudeCode',
40+
env: 'sidebar.env',
41+
docker: 'sidebar.docker',
42+
architecture: 'sidebar.architecture',
43+
systemContext: 'sidebar.systemContext',
44+
requestPipeline: 'sidebar.requestPipeline',
45+
logging: 'sidebar.logging',
46+
pipeline: 'sidebar.pipeline',
47+
metrics: 'sidebar.metrics',
48+
ttftTps: 'sidebar.ttftTps',
49+
tokens: 'sidebar.tokens',
50+
} as const

0 commit comments

Comments
 (0)