Internationalization
ubean uses vue-i18n 11 (Composition API, legacy: false) on the Vue side and @intlify/core in Hono handlers. Language routing is compact prefixes compiled once by compileLocalePaths() and shared by vue-router and Hono.
useI18n comes from vue-i18n directly (auto-import requires autoImports: { vueI18n: true } in ubean.config.ts — off by default; otherwise import it explicitly); destructure t from the returned composer. Framework helpers like setLocale are imported from ubean/client.
Setup
Configure everything in ubean.config.ts. Middleware is mounted by createUbeanApp automatically — you do not need 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' // default
},
vueI18n: {
fallbackLocale: 'en'
}
}
});locales: ['en', 'zh'] is normalized to { code } objects. i18n: false (or empty locales) disables routing and middleware.
language is BCP 47 for <html lang> and hreflang. Locale codes are reserved URL segments: pages/[id].vue will not match /zh if zh is a locale code.
Locale files
Put messages in src/locales/**/*.{json,json5,yaml,yml,ts,js}. Syntax is Intlify (not 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!"
}Messages load lazily via loadLocale(code). SSR preloads the current locale + fallback; __UBEAN_LOCALE__ serializes only those two catalogs.
Vue usage
<script setup lang="ts">
const { t, d, n, locale } = useI18n();
async function toZh() {
await setLocale('zh'); // load + cookie + router.replace
}
</script>
<template>
<h1>{{ t('hello') }}</h1>
<p>{{ t('greeting', { name: 'World' }) }}</p>
<button @click="toZh">中文</button>
<p>Current: {{ locale }}</p>
</template>Switch language with framework setLocale, not locale.value = 'zh'. setLocale loads messages, writes the cookie, and navigates (no_prefix skips navigation).
Language switcher buttons should iterate config locales (getI18nRuntimeConfig()?.locales), not vue-i18n availableLocales (that list is only loaded catalogs: current + fallback after hydrate).
<script setup lang="ts">
const codes = getI18nRuntimeConfig()?.locales ?? [];
</script>
<template>
<button v-for="code in codes" :key="code" @click="setLocale(code)">
{{ code }}
</button>
</template>Paths and <Link>
<script setup lang="ts">
const localePath = useLocalePath();
const switchLocalePath = useSwitchLocalePath();
</script>
<template>
<Link to="/about">About</Link>
<!-- zh → /zh/about ; default en → /about -->
<Link to="/about" locale="en">English About</Link>
</template>useLocalePath('/about', 'zh') → /zh/about. useSwitchLocalePath('zh') rewrites the current route.
Typed keys
ubean prepare / dev / build generate .ubean/i18n.d.ts from the default locale JSON (DefineLocaleMessage). Then t('missing') is a type error. You can also hand-write:
declare module 'vue-i18n' {
export interface DefineLocaleMessage {
hello: string;
}
}API routes / handlers
In handlers, t() / d() / n() from ubean/i18n read the request AsyncLocalStorage scope. Outside a request they throw — they never fall back to a process-global 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/, /_, /__ still enter ALS (cookie / Accept-Language) but do not rewrite the URL.
Cloudflare Workers need nodejs_compat for AsyncLocalStorage. If ALS is unavailable, t() throws; read c.get('locale') instead.
Routing strategies
Hono and vue-router share the same compiled paths (defaultLocale: 'en', locales en, zh, page /about):
| strategy | vue-router | Hono |
|---|---|---|
prefix_except_default (default) | /:locale(zh)?/about | /about, /zh/about (no /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 |
Detection order (prefixed strategies): URL prefix → cookie → Accept-Language → defaultLocale. Cookie is written. Default redirectOn: 'root': only / 302s from Accept-Language; /about does not. prefix still 302s unprefixed content regardless of redirectOn.
SEO
SSR HTML automatically sets:
<html lang>/<html dir>link rel="alternate" hreflang(including language-group catch-all andx-default)canonical(prefix_and_defaultpoints the default locale at the unprefixed URL)og:locale/og:locale:alternate
useLocaleHead() updates head after client-side locale switches. Set i18n.baseUrl for absolute hreflang hrefs.