国际化

ubean 在 Vue 侧使用 vue-i18n 11(Composition API,必须 legacy: false),在 Hono handler 中使用 @intlify/core。语言路由是 约束前缀:由 compileLocalePaths() 编译一次,vue-router 与 Hono 共用同一张 path 表。

useI18n 直接从 vue-i18n 导入(自动导入需在 ubean.config.ts 设置 autoImports: { vueI18n: true },默认关闭;否则请显式导入),t 从其返回的 composer 解构;setLocale 等 ubean 封装从 ubean/client 导入。

配置

全部写在 ubean.config.ts。检测中间件由 createUbeanApp 自动挂载,不需要 src/middleware/02.i18n.ts。

// ubean.config.ts
import { defineConfig } from 'ubean';

export default defineConfig({
  i18n: {
    defaultLocale: 'en',
    locales: [
      { code: 'en', language: 'en', name: 'English' },
      { code: 'zh', language: 'zh-CN', name: '简体中文' }
    ],
    strategy: 'prefix_except_default',
    baseUrl: 'https://example.com',
    detectBrowserLanguage: {
      cookieName: 'ubean_locale',
      redirectOn: 'root' // 默认
    },
    vueI18n: {
      fallbackLocale: 'en'
    }
  }
});

locales: ['en', 'zh'] 会正规化为 { code } 对象。i18n: false(或 locales 为空)关闭路由改写和中间件。

language 是 BCP 47,用于 <html lang> 和 hreflang。locale code 是 URL 保留段:若 zh 是语言 code,pages/[id].vue 不会把 /zh 当成 id。

文案文件

约定目录 src/locales/**/*.{json,json5,yaml,yml,ts,js}。消息语法是 Intlify(不是 ICU):{name}、@:key、a | b | {n} c。

// src/locales/en.json
{
  "hello": "Hello",
  "welcome": "Welcome to ubean",
  "greeting": "Hello, {name}!",
  "items": "no items | one item | {count} items",
  "app": { "name": "ubean" },
  "linked": "Welcome to @:app.name!"
}

通过 loadLocale(code) 懒加载。SSR 只预载当前 locale + fallback;__UBEAN_LOCALE__ 只序列化这两份。

Vue 用法

<script setup lang="ts">
const { t, d, n, locale } = useI18n();

async function toZh() {
  await setLocale('zh'); // 加载文案 + 写 cookie + router.replace
}
</script>

<template>
  <h1>{{ t('hello') }}</h1>
  <p>{{ t('greeting', { name: 'World' }) }}</p>
  <button @click="toZh">中文</button>
  <p>当前:{{ locale }}</p>
</template>

切换语言必须走框架 setLocale,不要直接赋 locale.value = 'zh'。setLocale 会加载文案、写 cookie、再导航(no_prefix 跳过导航)。

语言切换按钮应遍历 配置里的 locales(getI18nRuntimeConfig()?.locales),不要用 vue-i18n 的 availableLocales(水合后通常只有当前 + fallback)。

<script setup lang="ts">
const codes = getI18nRuntimeConfig()?.locales ?? [];
</script>

<template>
  <button v-for="code in codes" :key="code" @click="setLocale(code)">
    {{ code }}
  </button>
</template>

路径与 <Link>

<script setup lang="ts">
const localePath = useLocalePath();
const switchLocalePath = useSwitchLocalePath();
</script>

<template>
  <Link to="/about">About</Link>
  <!-- zh → /zh/about ;默认 en → /about -->
  <Link to="/about" locale="en">English About</Link>
</template>

useLocalePath('/about', 'zh') → /zh/about。useSwitchLocalePath('zh') 改写当前路由。

类型化 key

ubean prepare / dev / build 会根据默认 locale 的 JSON 生成 .ubean/i18n.d.ts(DefineLocaleMessage),此时 t('missing') 会报类型错误。也可以手写 module augmentation:

declare module 'vue-i18n' {
  export interface DefineLocaleMessage {
    hello: string;
  }
}

API / handler

handler 里从 ubean/i18n 导入的 t() / d() / n() 读请求 AsyncLocalStorage。没有请求作用域时抛错,禁止回落进程全局 locale。

import { defineHandler } from 'ubean/server';
import { t, getRequestLocale } from 'ubean/i18n';

export const GET = defineHandler(c => {
  const locale = getRequestLocale(c);
  return c.json({ locale, hello: t('hello') });
});

/api/、/_、/__ 仍进入 ALS(cookie / Accept-Language),但不改 URL。

Cloudflare Workers 需要 nodejs_compat 才能用 AsyncLocalStorage。ALS 不可用时 t() 抛错;请改读 c.get('locale')。

路由策略

Hono 与 vue-router 共用同一编译结果(defaultLocale: 'en',locales en, zh,页面 /about):

strategyvue-routerHono
prefix_except_default(默认)/:locale(zh)?/about/about、/zh/about(无 /en/about)
prefix/:locale(en|zh)/about/en/about、/zh/about
prefix_and_default/:locale(en|zh)?/about/about、/en/about、/zh/about
no_prefix/about/about

检测顺序(有前缀的 strategy):URL 前缀 → cookie → Accept-Language → defaultLocale。Cookie 会写入。默认 redirectOn: 'root':只有 / 会因 header 302;/about 不会。prefix 策略访问无前缀内容时无论 redirectOn 都会 302。

SEO

SSR HTML 自动设置:

  • <html lang> / <html dir>
  • link rel="alternate" hreflang(含语言组 catchall 与 x-default)
  • canonical(prefix_and_default 下默认语言指向 无前缀 URL)
  • og:locale / og:locale:alternate

客户端切换后可用 useLocaleHead() 更新 head。请配置 i18n.baseUrl 以生成绝对 hreflang。