Ver código fonte

Add browser language detection for SSR support

- Implemented locale detection during server-side rendering using cookies and Accept-Language header.
- Configured cookie key and fallback locale to enhance user experience across different languages.
0es 2 meses atrás
pai
commit
36d3f12d03
2 arquivos alterados com 337 adições e 0 exclusões
  1. 329 0
      app/pages/test/cookie.vue
  2. 8 0
      nuxt.config.ts

+ 329 - 0
app/pages/test/cookie.vue

@@ -0,0 +1,329 @@
+<script setup lang="ts">
+definePageMeta({
+  auth: false,
+})
+
+type CookieItem = {
+  name: string
+  value: string
+  decodedValue?: string
+  valueLength: number
+}
+
+type CookieSsrSnapshot = {
+  serverCollectedAt: string
+  url: string
+  method: string
+  clientIp: string | null
+  cookieHeader: string
+  cookieHeaderLength: number
+  cookies: CookieItem[]
+  headers: Record<string, string>
+}
+
+function safeDecodeURIComponent(value: string): string | undefined {
+  try {
+    return decodeURIComponent(value)
+  }
+  catch {
+    return undefined
+  }
+}
+
+function parseCookieHeader(cookieHeader: string): CookieItem[] {
+  if (!cookieHeader)
+    return []
+
+  return cookieHeader
+    .split(';')
+    .map(part => part.trim())
+    .filter(Boolean)
+    .map((part) => {
+      const idx = part.indexOf('=')
+      const name = (idx >= 0 ? part.slice(0, idx) : part).trim()
+      const value = (idx >= 0 ? part.slice(idx + 1) : '').trim()
+      const decodedValue = value ? safeDecodeURIComponent(value) : undefined
+
+      return {
+        name,
+        value,
+        decodedValue,
+        valueLength: value.length,
+      }
+    })
+}
+
+function collectSsrSnapshot(): CookieSsrSnapshot {
+  const url = useRequestURL()
+  const event = useRequestEvent()
+
+  const headersRaw = useRequestHeaders([
+    'cookie',
+    'user-agent',
+    'accept-language',
+    'referer',
+    'host',
+    'x-forwarded-for',
+    'x-real-ip',
+  ])
+
+  const headers: Record<string, string> = {}
+  for (const [k, v] of Object.entries(headersRaw)) {
+    if (typeof v === 'string')
+      headers[k] = v
+  }
+
+  const cookieHeader = headersRaw.cookie || ''
+  const cookies = parseCookieHeader(cookieHeader)
+
+  const xff = headersRaw['x-forwarded-for']
+  const clientIp = (xff ? xff.split(',')[0]?.trim() : null)
+    || headersRaw['x-real-ip']
+    || event?.node?.req?.socket?.remoteAddress
+    || null
+
+  return {
+    serverCollectedAt: new Date().toISOString(),
+    url: url.toString(),
+    method: event?.node?.req?.method || 'UNKNOWN',
+    clientIp,
+    cookieHeader,
+    cookieHeaderLength: cookieHeader.length,
+    cookies,
+    headers,
+  }
+}
+
+const importMetaServer = import.meta.server
+
+const ssrSnapshot = useState<CookieSsrSnapshot | null>('test-cookie-ssr-snapshot', () => {
+  if (!import.meta.server)
+    return null
+  return collectSsrSnapshot()
+})
+
+const hydrated = ref(false)
+const clientNow = ref(new Date().toISOString())
+
+onMounted(() => {
+  hydrated.value = true
+  clientNow.value = new Date().toISOString()
+})
+</script>
+
+<template>
+  <div class="cookie-page">
+    <h1 class="title">
+      Cookie SSR 调试
+    </h1>
+
+    <div class="meta">
+      <div class="meta__row">
+        <span class="meta__label">当前环境</span>
+        <span class="meta__value">{{ importMetaServer ? 'server' : 'client' }}</span>
+      </div>
+      <div class="meta__row">
+        <span class="meta__label">客户端已挂载</span>
+        <span class="meta__value">{{ hydrated ? 'yes' : 'no' }}</span>
+      </div>
+      <div class="meta__row">
+        <span class="meta__label">客户端时间</span>
+        <span class="meta__value">{{ clientNow }}</span>
+      </div>
+    </div>
+
+    <van-divider />
+
+    <template v-if="ssrSnapshot">
+      <h2 class="section-title">
+        原始 Cookie Header
+      </h2>
+      <pre class="code">{{ ssrSnapshot.cookieHeader || '(empty)' }}</pre>
+
+      <van-divider />
+
+      <h2 class="section-title">
+        解析后的 Cookies
+      </h2>
+
+      <template v-if="ssrSnapshot.cookies.length">
+        <div class="table-wrap">
+          <table class="table">
+            <thead>
+              <tr>
+                <th>name</th>
+                <th>value</th>
+                <th>decodedValue</th>
+                <th>length</th>
+              </tr>
+            </thead>
+            <tbody>
+              <tr
+                v-for="c in ssrSnapshot.cookies"
+                :key="c.name"
+              >
+                <td class="mono">
+                  {{ c.name }}
+                </td>
+                <td class="mono">
+                  {{ c.value }}
+                </td>
+                <td class="mono">
+                  {{ c.decodedValue ?? '-' }}
+                </td>
+                <td class="mono">
+                  {{ c.valueLength }}
+                </td>
+              </tr>
+            </tbody>
+          </table>
+        </div>
+      </template>
+      <template v-else>
+        <div class="empty">
+          未收到 Cookie(请求头里没有 cookie 字段或为空)。
+        </div>
+      </template>
+
+      <van-divider />
+
+      <h2 class="section-title">
+        Cookie 相关请求头(SSR 采样)
+      </h2>
+
+      <pre class="code">{{ JSON.stringify(ssrSnapshot.headers, null, 2) }}</pre>
+
+      <van-divider />
+
+      <h2 class="section-title">
+        SSR 快照(服务端渲染时采集并注入到客户端)
+      </h2>
+
+      <van-cell-group inset>
+        <van-cell
+          title="SSR 采集时间"
+          :value="ssrSnapshot.serverCollectedAt"
+        />
+        <van-cell
+          title="请求 URL"
+          :value="ssrSnapshot.url"
+        />
+        <van-cell
+          title="请求方法"
+          :value="ssrSnapshot.method"
+        />
+        <van-cell
+          title="推测客户端 IP"
+          :value="ssrSnapshot.clientIp || '-'"
+        />
+        <van-cell
+          title="Cookie Header 长度"
+          :value="String(ssrSnapshot.cookieHeaderLength)"
+        />
+        <van-cell
+          title="Cookie 数量(解析后)"
+          :value="String(ssrSnapshot.cookies.length)"
+        />
+      </van-cell-group>
+    </template>
+
+    <template v-else>
+      <van-empty description="未获取到 SSR 快照(可能是纯 CSR 导航且未命中 SSR,或当前环境不可用)。" />
+    </template>
+  </div>
+</template>
+
+<style scoped>
+.cookie-page {
+  padding: 16px;
+}
+
+.title {
+  font-size: 18px;
+  font-weight: 700;
+  margin: 0 0 12px;
+}
+
+.section-title {
+  font-size: 14px;
+  font-weight: 700;
+  margin: 12px 0 8px;
+}
+
+.meta {
+  display: grid;
+  grid-template-columns: 1fr;
+  gap: 6px;
+  padding: 12px;
+  border-radius: 10px;
+  background: rgba(0, 0, 0, 0.03);
+}
+
+.meta__row {
+  display: flex;
+  justify-content: space-between;
+  gap: 12px;
+  font-size: 12px;
+}
+
+.meta__label {
+  opacity: 0.7;
+}
+
+.meta__value {
+  font-weight: 600;
+  word-break: break-all;
+  text-align: right;
+}
+
+.code {
+  padding: 12px;
+  border-radius: 10px;
+  background: rgba(0, 0, 0, 0.04);
+  overflow: auto;
+  font-size: 12px;
+  line-height: 1.45;
+  margin: 0;
+}
+
+.table-wrap {
+  overflow: auto;
+  border-radius: 10px;
+  background: rgba(0, 0, 0, 0.02);
+  padding: 8px;
+}
+
+.table {
+  width: 100%;
+  border-collapse: collapse;
+  font-size: 12px;
+}
+
+.table th,
+.table td {
+  padding: 8px;
+  border-bottom: 1px solid rgba(0, 0, 0, 0.08);
+  text-align: left;
+  vertical-align: top;
+  white-space: nowrap;
+}
+
+.mono {
+  font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
+}
+
+.empty {
+  padding: 12px;
+  border-radius: 10px;
+  background: rgba(0, 0, 0, 0.03);
+  font-size: 12px;
+  opacity: 0.8;
+}
+
+.notes {
+  margin: 0;
+  padding-left: 18px;
+  font-size: 12px;
+  line-height: 1.6;
+}
+</style>

+ 8 - 0
nuxt.config.ts

@@ -94,6 +94,14 @@ export default defineNuxtConfig({
       { code: 'id', name: 'Bahasa Indonesia', file: 'id.json' },
       { code: 'zh', name: '简体中文', file: 'zh.json' },
     ],
+    // Ensure locale can be determined during SSR:
+    // - 1st priority: cookie (user preference)
+    // - fallback: Accept-Language header
+    detectBrowserLanguage: {
+      useCookie: true,
+      cookieKey: 'GAMI-cookie_app_locale',
+      fallbackLocale: 'en',
+    },
   },
   sentry: {
     enabled: process.env.NUXT_PUBLIC_ENV === 'production',