@ubean/i18n 0.6.2 → 0.6.4
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/README.md +313 -0
- package/README.zh-CN.md +313 -0
- package/package.json +3 -3
package/README.md
ADDED
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
# @ubean/i18n
|
|
2
|
+
|
|
3
|
+
**English | [中文](./README.zh-CN.md)**
|
|
4
|
+
|
|
5
|
+
i18n engine for ubean — compact locale routing compiled once and shared by Hono and vue-router, plus AsyncLocalStorage-scoped `t()` / `d()` / `n()` for handlers.
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
import { compileLocalePaths } from '@ubean/i18n';
|
|
9
|
+
|
|
10
|
+
const compiled = compileLocalePaths('/about', {
|
|
11
|
+
defaultLocale: 'en',
|
|
12
|
+
locales: ['en', 'zh'],
|
|
13
|
+
strategy: 'prefix_except_default'
|
|
14
|
+
});
|
|
15
|
+
// compiled.vuePath → '/:locale(zh)?/about'
|
|
16
|
+
// compiled.hono → [{ path: '/about', locale: 'en', isDefault: true }, { path: '/zh/about', locale: 'zh', isDefault: false }]
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Table of Contents
|
|
20
|
+
|
|
21
|
+
- [Package Positioning](#package-positioning)
|
|
22
|
+
- [Install](#install)
|
|
23
|
+
- [Quick Start](#quick-start)
|
|
24
|
+
- [Locale Routing Strategies](#locale-routing-strategies)
|
|
25
|
+
- [Path Compilation & Helpers](#path-compilation--helpers)
|
|
26
|
+
- [Request Middleware](#request-middleware)
|
|
27
|
+
- [Messages in AsyncLocalStorage](#messages-in-asynclocalstorage)
|
|
28
|
+
- [Locale Head (hreflang / canonical)](#locale-head-hreflang--canonical)
|
|
29
|
+
- [Browser-Safe Subpath](#browser-safe-subpath)
|
|
30
|
+
- [Type Safety](#type-safety)
|
|
31
|
+
- [Architecture Notes](#architecture-notes)
|
|
32
|
+
|
|
33
|
+
## Package Positioning
|
|
34
|
+
|
|
35
|
+
`@ubean/i18n` is the **single owner of locale routing** in ubean, and the home of the **request-scoped translation API**:
|
|
36
|
+
|
|
37
|
+
| Layer | Responsibility |
|
|
38
|
+
| ----------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
39
|
+
| `@ubean/i18n` (this package) | The four compact locale-routing strategies, `compileLocalePaths`, URL helpers, locale detection (cookie / `Accept-Language`), `createI18nMiddleware`, ALS `t()` / `d()` / `n()`, locale catalogs, and the hreflang/canonical head builder |
|
|
40
|
+
| `@ubean/client` (framework runtime) | Vue-side wiring over the vue-i18n 11 composer: `setLocale`, `useLocalePath`, `useSwitchLocalePath`, `useLocaleHead`, `getI18nRuntimeConfig` |
|
|
41
|
+
| `@ubean/app` | Mounts `createI18nMiddleware` inside `createUbeanApp` — you do not wire the middleware yourself |
|
|
42
|
+
| `@ubean/config` | Resolves the `i18n` field of `ubean.config.ts` into a `LocaleRoutingConfig` (`resolveI18nConfig`) |
|
|
43
|
+
|
|
44
|
+
**Included:**
|
|
45
|
+
|
|
46
|
+
- `compileLocalePaths(pagePath, cfg)` — the shared vue-router + Hono path compiler (`CompiledLocalePath`)
|
|
47
|
+
- `getVueLocaleParam` / `toVueRouterLocalePath` / `extractLocaleFromPath` / `localizePath` / `switchLocalePath`
|
|
48
|
+
- `createI18nMiddleware(options)` + `getRequestLocale(c)` / `getPathWithoutLocale(c)`
|
|
49
|
+
- ALS translation scope: `t` / `d` / `n`, `runWithI18n`, `getI18nScope`, `createRequestContext`
|
|
50
|
+
- Locale catalog management: `setLocaleMessages` / `getLocaleMessages` / `mergeLocaleMessages`, `setLocaleMeta` / `getLocaleMeta`, `setFallbackLocale` / `getFallbackLocale`, `registerLocaleLoader` / `ensureLocaleMessages`
|
|
51
|
+
- Locale metadata readers: `listLocaleCodes`, `getRegisteredLocalesMeta`, `getLocaleDir`, `getLocaleName`
|
|
52
|
+
- Detection helpers: `detectLocaleFromAcceptLanguage`, `parseLocaleCookie`, `serializeLocaleCookie`
|
|
53
|
+
- SEO head builder: `buildLocaleHead` (`htmlAttrs` / `link` / `meta`)
|
|
54
|
+
- Browser-safe subset at `@ubean/i18n/browser`
|
|
55
|
+
|
|
56
|
+
**NOT included** (owned next door):
|
|
57
|
+
|
|
58
|
+
- The vue-i18n composer itself — `useI18n` is imported from `vue-i18n` directly (Composition API, `legacy: false`)
|
|
59
|
+
- App construction and middleware mounting (`createUbeanApp` in `@ubean/app`)
|
|
60
|
+
- SEO meta rendering / `<Head>` (`@ubean/seo`); this package only produces the locale tag descriptors
|
|
61
|
+
- Locale file discovery and catalog loading on disk (`@ubean/scan` + the build pipeline, surfaced as `loadMessages`)
|
|
62
|
+
|
|
63
|
+
**Runtime dependency whitelist (main entry): `@intlify/core`, `@ubean/shared`, `hono`.** No `vue` and no `vue-i18n` static import — the Vue composer is wired by `@ubean/client`.
|
|
64
|
+
|
|
65
|
+
This package declares **no optional peers**: nothing degrades at runtime when an optional package is absent.
|
|
66
|
+
|
|
67
|
+
## Install
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
pnpm add @ubean/i18n
|
|
71
|
+
# Vue side (installed by the application, not by this package):
|
|
72
|
+
pnpm add vue-i18n # vue-i18n 11, Composition API (legacy: false)
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Quick Start
|
|
76
|
+
|
|
77
|
+
**1. Configure** the `i18n` field in `ubean.config.ts` — the middleware is mounted automatically:
|
|
78
|
+
|
|
79
|
+
```ts
|
|
80
|
+
// ubean.config.ts
|
|
81
|
+
import { defineConfig } from 'ubean';
|
|
82
|
+
|
|
83
|
+
export default defineConfig({
|
|
84
|
+
i18n: {
|
|
85
|
+
defaultLocale: 'en',
|
|
86
|
+
locales: [
|
|
87
|
+
{ code: 'en', language: 'en', name: 'English' },
|
|
88
|
+
{ code: 'zh', language: 'zh-CN', name: '简体中文' }
|
|
89
|
+
],
|
|
90
|
+
strategy: 'prefix_except_default',
|
|
91
|
+
detectBrowserLanguage: { cookieName: 'ubean_locale', redirectOn: 'root' }
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
**2. Translate in handlers** — `t()` reads the request AsyncLocalStorage scope:
|
|
97
|
+
|
|
98
|
+
```ts
|
|
99
|
+
import { t, getRequestLocale } from 'ubean/i18n';
|
|
100
|
+
// src/routes/hello.ts
|
|
101
|
+
import { defineHandler } from 'ubean/server';
|
|
102
|
+
|
|
103
|
+
export const GET = defineHandler(c => {
|
|
104
|
+
return c.json({ locale: getRequestLocale(c), hello: t('hello') });
|
|
105
|
+
});
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
**3. Compile paths** once and let both routers consume the same result:
|
|
109
|
+
|
|
110
|
+
```ts
|
|
111
|
+
import { compileLocalePaths } from '@ubean/i18n';
|
|
112
|
+
|
|
113
|
+
const cfg = { defaultLocale: 'en', locales: ['en', 'zh'], strategy: 'prefix_except_default' } as const;
|
|
114
|
+
|
|
115
|
+
compileLocalePaths('/about', cfg);
|
|
116
|
+
// → { vuePath: '/:locale(zh)?/about', hono: [{ path: '/about', … }, { path: '/zh/about', … }] }
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## Locale Routing Strategies
|
|
120
|
+
|
|
121
|
+
Four compact strategies; `prefix_except_default` is the framework default. With `defaultLocale: 'en'`, locales `en, zh`, page `/about`:
|
|
122
|
+
|
|
123
|
+
| Strategy | vue-router | Hono |
|
|
124
|
+
| ----------------------- | ------------------------- | -------------------------------------- |
|
|
125
|
+
| `prefix_except_default` | `/:locale(zh)?/about` | `/about`, `/zh/about` (no `/en/about`) |
|
|
126
|
+
| `prefix` | `/:locale(en\|zh)/about` | `/en/about`, `/zh/about` |
|
|
127
|
+
| `prefix_and_default` | `/:locale(en\|zh)?/about` | `/about`, `/en/about`, `/zh/about` |
|
|
128
|
+
| `no_prefix` | `/about` | `/about` |
|
|
129
|
+
|
|
130
|
+
```ts
|
|
131
|
+
import { getVueLocaleParam } from '@ubean/i18n';
|
|
132
|
+
|
|
133
|
+
const cfg = { defaultLocale: 'en', locales: ['en', 'zh'], strategy: 'prefix_except_default' } as const;
|
|
134
|
+
|
|
135
|
+
getVueLocaleParam(cfg); // ':locale(zh)?'
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Locale **codes** are reserved URL segments — `pages/[id].vue` will not match `/zh` when `zh` is a locale code.
|
|
139
|
+
|
|
140
|
+
## Path Compilation & Helpers
|
|
141
|
+
|
|
142
|
+
```ts
|
|
143
|
+
import {
|
|
144
|
+
compileLocalePaths,
|
|
145
|
+
extractLocaleFromPath,
|
|
146
|
+
localizePath,
|
|
147
|
+
switchLocalePath,
|
|
148
|
+
toVueRouterLocalePath
|
|
149
|
+
} from '@ubean/i18n';
|
|
150
|
+
|
|
151
|
+
const cfg = { defaultLocale: 'en', locales: ['en', 'zh'], strategy: 'prefix_except_default' } as const;
|
|
152
|
+
|
|
153
|
+
const { vuePath, hono } = compileLocalePaths('/about', cfg);
|
|
154
|
+
|
|
155
|
+
extractLocaleFromPath('/zh/about', cfg.locales);
|
|
156
|
+
// → { locale: 'zh', pathWithoutLocale: '/about' }
|
|
157
|
+
|
|
158
|
+
localizePath('/about', 'zh', cfg); // '/zh/about'
|
|
159
|
+
switchLocalePath('zh', '/about', cfg); // '/zh/about'
|
|
160
|
+
|
|
161
|
+
toVueRouterLocalePath('/about', getVueLocaleParam(cfg)); // '/:locale(zh)?/about'
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
- `compileLocalePaths(pagePath, cfg)` returns `{ vuePath, hono: HonoLocalePath[] }`; `hono` carries `{ path, locale, isDefault }` per locale.
|
|
165
|
+
- `switchLocalePath(locale, currentPath, cfg)` is `localizePath(currentPath, locale, cfg)` — pass the current route path to get the same route in another locale.
|
|
166
|
+
- These helpers are pure functions: no ALS, no request state, safe in the browser bundle.
|
|
167
|
+
|
|
168
|
+
## Request Middleware
|
|
169
|
+
|
|
170
|
+
`createI18nMiddleware(options)` is mounted by `createUbeanApp`; call it directly only in a custom app.
|
|
171
|
+
|
|
172
|
+
Detection order for prefixed strategies: **URL prefix → cookie → `Accept-Language` → `defaultLocale`**. The cookie is written back on every resolved request.
|
|
173
|
+
|
|
174
|
+
```ts
|
|
175
|
+
import { createI18nMiddleware } from '@ubean/i18n';
|
|
176
|
+
|
|
177
|
+
const middleware = createI18nMiddleware({
|
|
178
|
+
defaultLocale: 'en',
|
|
179
|
+
locales: ['en', 'zh'],
|
|
180
|
+
strategy: 'prefix_except_default',
|
|
181
|
+
detectBrowserLanguage: { cookieName: 'ubean_locale', redirectOn: 'root', alwaysRedirect: false }
|
|
182
|
+
});
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
Behavior details:
|
|
186
|
+
|
|
187
|
+
- Defaults: `cookieName: 'ubean_locale'`, `redirectOn: 'root'`, `alwaysRedirect: false`.
|
|
188
|
+
- `/api/`, `/_`, `/__` paths still enter the ALS scope (cookie / `Accept-Language`) but the URL is never rewritten.
|
|
189
|
+
- `prefix_except_default` 302s `/en/...` to the unprefixed path; `prefix` 302s unprefixed content to the preferred prefix regardless of `redirectOn`.
|
|
190
|
+
- `redirectOn: 'root'` 302s only `/`; `redirectOn: 'all'` also redirects deeper paths.
|
|
191
|
+
- The middleware sets `c.set('locale')`, `c.set('pathWithoutLocale')` and a `Content-Language` response header, and appends `Set-Cookie`.
|
|
192
|
+
|
|
193
|
+
```ts
|
|
194
|
+
import { getRequestLocale, getPathWithoutLocale } from 'ubean/i18n';
|
|
195
|
+
import { defineHandler } from 'ubean/server';
|
|
196
|
+
|
|
197
|
+
export const GET = defineHandler(c => {
|
|
198
|
+
return c.json({ locale: getRequestLocale(c), path: getPathWithoutLocale(c) });
|
|
199
|
+
});
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
The middleware stores the resolved locale and the unprefixed path on the context; `getPathWithoutLocale(c)` reads the latter.
|
|
203
|
+
|
|
204
|
+
## Messages in AsyncLocalStorage
|
|
205
|
+
|
|
206
|
+
Handler-side translation lives in an ALS scope. `t()` **throws** outside ALS — there is no process-global locale.
|
|
207
|
+
|
|
208
|
+
```ts
|
|
209
|
+
import { t, d, n, getI18nScope } from 'ubean/i18n';
|
|
210
|
+
import { defineHandler } from 'ubean/server';
|
|
211
|
+
|
|
212
|
+
export const GET = defineHandler(c => {
|
|
213
|
+
const scope = getI18nScope();
|
|
214
|
+
return c.json({
|
|
215
|
+
hello: t('hello'),
|
|
216
|
+
greeting: t('greeting', { name: 'World' }),
|
|
217
|
+
when: d(new Date()),
|
|
218
|
+
count: n(1200)
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
Catalog and metadata APIs:
|
|
224
|
+
|
|
225
|
+
```ts
|
|
226
|
+
import {
|
|
227
|
+
ensureLocaleMessages,
|
|
228
|
+
getFallbackLocale,
|
|
229
|
+
getLocaleDir,
|
|
230
|
+
getLocaleMeta,
|
|
231
|
+
registerLocaleLoader,
|
|
232
|
+
setFallbackLocale,
|
|
233
|
+
setLocaleMessages,
|
|
234
|
+
setLocaleMeta
|
|
235
|
+
} from '@ubean/i18n';
|
|
236
|
+
|
|
237
|
+
setFallbackLocale('en');
|
|
238
|
+
setLocaleMessages('en', { hello: 'Hello' });
|
|
239
|
+
|
|
240
|
+
registerLocaleLoader(async locale => {
|
|
241
|
+
const messages = await import(`../locales/${locale}.json`);
|
|
242
|
+
setLocaleMessages(locale, messages.default);
|
|
243
|
+
});
|
|
244
|
+
await ensureLocaleMessages('zh', getFallbackLocale());
|
|
245
|
+
|
|
246
|
+
setLocaleMeta('zh', { code: 'zh', language: 'zh-CN', name: '简体中文', dir: 'ltr' });
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
Also exported: `getLocaleMessages` / `mergeLocaleMessages`, `listLocaleCodes`, `getRegisteredLocalesMeta`, `getLocaleName`. `createRequestContext(locale, fallback)` builds the scope object consumed by `runWithI18n(scope, fn)` for tests and internal callers; `getAlsLocale()` (the index alias of `getRequestLocale()`) reads the current locale without a Hono context.
|
|
250
|
+
|
|
251
|
+
## Locale Head (hreflang / canonical)
|
|
252
|
+
|
|
253
|
+
`buildLocaleHead` turns the resolved routing state into head tags — no Vue, no unhead dependency:
|
|
254
|
+
|
|
255
|
+
```ts
|
|
256
|
+
import { buildLocaleHead } from '@ubean/i18n';
|
|
257
|
+
|
|
258
|
+
const tags = buildLocaleHead({
|
|
259
|
+
path: '/about',
|
|
260
|
+
locale: 'zh',
|
|
261
|
+
locales: [
|
|
262
|
+
{ code: 'en', language: 'en', name: 'English', isDefault: true },
|
|
263
|
+
{ code: 'zh', language: 'zh-CN', name: '简体中文', dir: 'ltr' }
|
|
264
|
+
],
|
|
265
|
+
routing: { defaultLocale: 'en', locales: ['en', 'zh'], strategy: 'prefix_and_default' },
|
|
266
|
+
baseUrl: 'https://example.com'
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
tags.htmlAttrs; // { lang, dir }
|
|
270
|
+
tags.link; // rel=alternate hreflang entries (incl. x-default) + canonical
|
|
271
|
+
tags.meta; // og:locale / og:locale:alternate
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
- `hreflang` alternates include the language-group catch-all entry and `x-default`.
|
|
275
|
+
- With `prefix_and_default`, the default locale's canonical points at the **unprefixed** URL.
|
|
276
|
+
- Set `baseUrl` to emit absolute hrefs.
|
|
277
|
+
|
|
278
|
+
## Browser-Safe Subpath
|
|
279
|
+
|
|
280
|
+
`@ubean/i18n/browser` exports the request-free subset, so client code never pulls in Hono or the ALS runtime:
|
|
281
|
+
|
|
282
|
+
```ts
|
|
283
|
+
import {
|
|
284
|
+
buildLocaleHead,
|
|
285
|
+
compileLocalePaths,
|
|
286
|
+
detectLocaleFromAcceptLanguage,
|
|
287
|
+
extractLocaleFromPath,
|
|
288
|
+
getVueLocaleParam,
|
|
289
|
+
localizePath,
|
|
290
|
+
parseLocaleCookie,
|
|
291
|
+
serializeLocaleCookie,
|
|
292
|
+
switchLocalePath,
|
|
293
|
+
toVueRouterLocalePath
|
|
294
|
+
} from '@ubean/i18n/browser';
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
Also exported there: the routing types (`I18nRoutingStrategy`, `LocaleRoutingConfig`, `CompiledLocalePath`, `HonoLocalePath`, `I18nLocaleMeta`) and the head types (`LocaleHeadInput`, `LocaleHeadTags`).
|
|
298
|
+
|
|
299
|
+
## Type Safety
|
|
300
|
+
|
|
301
|
+
- Routing types: `I18nRoutingStrategy` (`'prefix' | 'prefix_except_default' | 'prefix_and_default' | 'no_prefix'`), `LocaleRoutingConfig`, `CompiledLocalePath`, `HonoLocalePath`, `I18nMiddlewareOptions`, `I18nDetectOptions`, `I18nLocaleMeta`.
|
|
302
|
+
- Runtime types: `LocaleMessages`, `I18nRequestScope`, `I18nCoreContext`.
|
|
303
|
+
- Head types: `LocaleHeadInput`, `LocaleHeadTags`.
|
|
304
|
+
- `LocaleRoutingConfig` is structurally compatible with the resolved `i18n` config, so a `as const` config object can be passed to the pure helpers without casts.
|
|
305
|
+
- Message-key typing is generated by the build pipeline into `.ubean/i18n.d.ts`, which augments vue-i18n's `DefineLocaleMessage` from the default locale catalog. Include `.ubean/*.d.ts` in `tsconfig.json`.
|
|
306
|
+
|
|
307
|
+
## Architecture Notes
|
|
308
|
+
|
|
309
|
+
- **One compiler, two routers.** `compileLocalePaths` is the only place that knows the prefix rules; Hono route registration and vue-router route generation both consume its output. Changing a strategy changes one function, not two route tables.
|
|
310
|
+
- **No process-global locale.** `t()` / `d()` / `n()` read ALS strictly; outside a request scope they throw instead of silently falling back to a default locale. Cloudflare Workers need `nodejs_compat` for `AsyncLocalStorage`.
|
|
311
|
+
- **Server engine vs Vue engine.** Handlers use `@intlify/core`; the Vue side uses vue-i18n 11. The two share catalogs, not instances — this package never imports `vue-i18n`. Middleware is framework-mounted by `createUbeanApp`, so `src/middleware/02.i18n.ts` is not part of the convention.
|
|
312
|
+
- **Removed, with no compatibility layer:** `defineLocale`, `addLocale`, `mergeLocale`, `clearLocales`, `formatDate`, `formatNumber`, `formatCurrency`, `formatRelativeTime`, `formatList`, and a process-global `useI18n()` from this package. Use vue-i18n's `d` / `n` and the ALS `t()` instead.
|
|
313
|
+
- **Decision record:** [`docs/adr/0009-i18n-engine-and-compact-locale-routing.md`](../../docs/adr/0009-i18n-engine-and-compact-locale-routing.md). A multilingual SSG walkthrough lives in [`examples/ssg-catchall`](../../examples/ssg-catchall).
|
package/README.zh-CN.md
ADDED
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
# @ubean/i18n
|
|
2
|
+
|
|
3
|
+
**[English](./README.md) | 中文**
|
|
4
|
+
|
|
5
|
+
ubean 的 i18n 引擎 —— 紧凑语言前缀路由只编译一次,Hono 与 vue-router 共用;handler 侧提供 AsyncLocalStorage 作用域的 `t()` / `d()` / `n()`。
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
import { compileLocalePaths } from '@ubean/i18n';
|
|
9
|
+
|
|
10
|
+
const compiled = compileLocalePaths('/about', {
|
|
11
|
+
defaultLocale: 'en',
|
|
12
|
+
locales: ['en', 'zh'],
|
|
13
|
+
strategy: 'prefix_except_default'
|
|
14
|
+
});
|
|
15
|
+
// compiled.vuePath → '/:locale(zh)?/about'
|
|
16
|
+
// compiled.hono → [{ path: '/about', locale: 'en', isDefault: true }, { path: '/zh/about', locale: 'zh', isDefault: false }]
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## 目录
|
|
20
|
+
|
|
21
|
+
- [包定位](#包定位)
|
|
22
|
+
- [安装](#安装)
|
|
23
|
+
- [快速开始](#快速开始)
|
|
24
|
+
- [语言路由策略](#语言路由策略)
|
|
25
|
+
- [路径编译与工具函数](#路径编译与工具函数)
|
|
26
|
+
- [请求中间件](#请求中间件)
|
|
27
|
+
- [AsyncLocalStorage 中的消息](#asynclocalstorage-中的消息)
|
|
28
|
+
- [语言 head(hreflang / canonical)](#语言-headhreflang--canonical)
|
|
29
|
+
- [浏览器安全子路径](#浏览器安全子路径)
|
|
30
|
+
- [类型安全](#类型安全)
|
|
31
|
+
- [架构说明](#架构说明)
|
|
32
|
+
|
|
33
|
+
## 包定位
|
|
34
|
+
|
|
35
|
+
`@ubean/i18n` 是 ubean 中**语言路由的唯一所有者**,也是**请求作用域翻译 API** 的所在地:
|
|
36
|
+
|
|
37
|
+
| 层 | 职责 |
|
|
38
|
+
| ----------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
39
|
+
| `@ubean/i18n`(本包) | 四种紧凑语言路由策略、`compileLocalePaths`、URL 工具、语言检测(cookie / `Accept-Language`)、`createI18nMiddleware`、ALS `t()` / `d()` / `n()`、语言目录,以及 hreflang/canonical head 构建器 |
|
|
40
|
+
| `@ubean/client`(框架运行时) | vue-i18n 11 composer 之上的 Vue 侧接线:`setLocale`、`useLocalePath`、`useSwitchLocalePath`、`useLocaleHead`、`getI18nRuntimeConfig` |
|
|
41
|
+
| `@ubean/app` | 在 `createUbeanApp` 内挂载 `createI18nMiddleware` —— 你不需要自己接中间件 |
|
|
42
|
+
| `@ubean/config` | 把 `ubean.config.ts` 的 `i18n` 字段解析为 `LocaleRoutingConfig`(`resolveI18nConfig`) |
|
|
43
|
+
|
|
44
|
+
**包含:**
|
|
45
|
+
|
|
46
|
+
- `compileLocalePaths(pagePath, cfg)` —— vue-router 与 Hono 共用的路径编译器(`CompiledLocalePath`)
|
|
47
|
+
- `getVueLocaleParam` / `toVueRouterLocalePath` / `extractLocaleFromPath` / `localizePath` / `switchLocalePath`
|
|
48
|
+
- `createI18nMiddleware(options)` + `getRequestLocale(c)` / `getPathWithoutLocale(c)`
|
|
49
|
+
- ALS 翻译作用域:`t` / `d` / `n`、`runWithI18n`、`getI18nScope`、`createRequestContext`
|
|
50
|
+
- 语言目录管理:`setLocaleMessages` / `getLocaleMessages` / `mergeLocaleMessages`、`setLocaleMeta` / `getLocaleMeta`、`setFallbackLocale` / `getFallbackLocale`、`registerLocaleLoader` / `ensureLocaleMessages`
|
|
51
|
+
- 语言元信息读取:`listLocaleCodes`、`getRegisteredLocalesMeta`、`getLocaleDir`、`getLocaleName`
|
|
52
|
+
- 检测工具:`detectLocaleFromAcceptLanguage`、`parseLocaleCookie`、`serializeLocaleCookie`
|
|
53
|
+
- SEO head 构建器:`buildLocaleHead`(`htmlAttrs` / `link` / `meta`)
|
|
54
|
+
- `@ubean/i18n/browser` 处的浏览器安全子集
|
|
55
|
+
|
|
56
|
+
**不包含**(由邻近包负责):
|
|
57
|
+
|
|
58
|
+
- vue-i18n composer 本身 —— `useI18n` 直接从 `vue-i18n` 导入(Composition API,`legacy: false`)
|
|
59
|
+
- 应用构建与中间件挂载(`@ubean/app` 的 `createUbeanApp`)
|
|
60
|
+
- SEO meta 渲染 / `<Head>`(`@ubean/seo`);本包只产出语言标签描述结构
|
|
61
|
+
- 语言文件发现与磁盘目录加载(`@ubean/scan` + 构建流水线,对外表现为 `loadMessages`)
|
|
62
|
+
|
|
63
|
+
**运行时依赖白名单(主入口):`@intlify/core`、`@ubean/shared`、`hono`。** 不静态导入 `vue`,也不静态导入 `vue-i18n` —— Vue composer 由 `@ubean/client` 接线。
|
|
64
|
+
|
|
65
|
+
本包声明**无 optional peer**:任何可选包缺失都不会造成运行时能力降级。
|
|
66
|
+
|
|
67
|
+
## 安装
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
pnpm add @ubean/i18n
|
|
71
|
+
# Vue 侧(由应用安装,不由本包安装):
|
|
72
|
+
pnpm add vue-i18n # vue-i18n 11,Composition API(legacy: false)
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## 快速开始
|
|
76
|
+
|
|
77
|
+
**1. 配置** `ubean.config.ts` 的 `i18n` 字段 —— 中间件会自动挂载:
|
|
78
|
+
|
|
79
|
+
```ts
|
|
80
|
+
// ubean.config.ts
|
|
81
|
+
import { defineConfig } from 'ubean';
|
|
82
|
+
|
|
83
|
+
export default defineConfig({
|
|
84
|
+
i18n: {
|
|
85
|
+
defaultLocale: 'en',
|
|
86
|
+
locales: [
|
|
87
|
+
{ code: 'en', language: 'en', name: 'English' },
|
|
88
|
+
{ code: 'zh', language: 'zh-CN', name: '简体中文' }
|
|
89
|
+
],
|
|
90
|
+
strategy: 'prefix_except_default',
|
|
91
|
+
detectBrowserLanguage: { cookieName: 'ubean_locale', redirectOn: 'root' }
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
**2. 在 handler 中翻译** —— `t()` 读取请求的 AsyncLocalStorage 作用域:
|
|
97
|
+
|
|
98
|
+
```ts
|
|
99
|
+
import { t, getRequestLocale } from 'ubean/i18n';
|
|
100
|
+
// src/routes/hello.ts
|
|
101
|
+
import { defineHandler } from 'ubean/server';
|
|
102
|
+
|
|
103
|
+
export const GET = defineHandler(c => {
|
|
104
|
+
return c.json({ locale: getRequestLocale(c), hello: t('hello') });
|
|
105
|
+
});
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
**3. 路径只编译一次**,让两个 router 消费同一份结果:
|
|
109
|
+
|
|
110
|
+
```ts
|
|
111
|
+
import { compileLocalePaths } from '@ubean/i18n';
|
|
112
|
+
|
|
113
|
+
const cfg = { defaultLocale: 'en', locales: ['en', 'zh'], strategy: 'prefix_except_default' } as const;
|
|
114
|
+
|
|
115
|
+
compileLocalePaths('/about', cfg);
|
|
116
|
+
// → { vuePath: '/:locale(zh)?/about', hono: [{ path: '/about', … }, { path: '/zh/about', … }] }
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## 语言路由策略
|
|
120
|
+
|
|
121
|
+
共四种紧凑策略;`prefix_except_default` 是框架默认值。以 `defaultLocale: 'en'`、locales `en, zh`、页面 `/about` 为例:
|
|
122
|
+
|
|
123
|
+
| 策略 | vue-router | Hono |
|
|
124
|
+
| ----------------------- | ------------------------- | --------------------------------------- |
|
|
125
|
+
| `prefix_except_default` | `/:locale(zh)?/about` | `/about`、`/zh/about`(无 `/en/about`) |
|
|
126
|
+
| `prefix` | `/:locale(en\|zh)/about` | `/en/about`、`/zh/about` |
|
|
127
|
+
| `prefix_and_default` | `/:locale(en\|zh)?/about` | `/about`、`/en/about`、`/zh/about` |
|
|
128
|
+
| `no_prefix` | `/about` | `/about` |
|
|
129
|
+
|
|
130
|
+
```ts
|
|
131
|
+
import { getVueLocaleParam } from '@ubean/i18n';
|
|
132
|
+
|
|
133
|
+
const cfg = { defaultLocale: 'en', locales: ['en', 'zh'], strategy: 'prefix_except_default' } as const;
|
|
134
|
+
|
|
135
|
+
getVueLocaleParam(cfg); // ':locale(zh)?'
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
语言 **code** 是保留的 URL 段 —— 当 `zh` 是语言 code 时,`pages/[id].vue` 不会匹配 `/zh`。
|
|
139
|
+
|
|
140
|
+
## 路径编译与工具函数
|
|
141
|
+
|
|
142
|
+
```ts
|
|
143
|
+
import {
|
|
144
|
+
compileLocalePaths,
|
|
145
|
+
extractLocaleFromPath,
|
|
146
|
+
localizePath,
|
|
147
|
+
switchLocalePath,
|
|
148
|
+
toVueRouterLocalePath
|
|
149
|
+
} from '@ubean/i18n';
|
|
150
|
+
|
|
151
|
+
const cfg = { defaultLocale: 'en', locales: ['en', 'zh'], strategy: 'prefix_except_default' } as const;
|
|
152
|
+
|
|
153
|
+
const { vuePath, hono } = compileLocalePaths('/about', cfg);
|
|
154
|
+
|
|
155
|
+
extractLocaleFromPath('/zh/about', cfg.locales);
|
|
156
|
+
// → { locale: 'zh', pathWithoutLocale: '/about' }
|
|
157
|
+
|
|
158
|
+
localizePath('/about', 'zh', cfg); // '/zh/about'
|
|
159
|
+
switchLocalePath('zh', '/about', cfg); // '/zh/about'
|
|
160
|
+
|
|
161
|
+
toVueRouterLocalePath('/about', getVueLocaleParam(cfg)); // '/:locale(zh)?/about'
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
- `compileLocalePaths(pagePath, cfg)` 返回 `{ vuePath, hono: HonoLocalePath[] }`;`hono` 每项为 `{ path, locale, isDefault }`。
|
|
165
|
+
- `switchLocalePath(locale, currentPath, cfg)` 即 `localizePath(currentPath, locale, cfg)` —— 传入当前路由路径即可得到另一语言下的同一路由。
|
|
166
|
+
- 这些工具是纯函数:不涉及 ALS、不依赖请求状态,放进浏览器产物是安全的。
|
|
167
|
+
|
|
168
|
+
## 请求中间件
|
|
169
|
+
|
|
170
|
+
`createI18nMiddleware(options)` 由 `createUbeanApp` 挂载;只有在自定义 app 中才需要直接调用它。
|
|
171
|
+
|
|
172
|
+
前缀策略的检测顺序:**URL 前缀 → cookie → `Accept-Language` → `defaultLocale`**。每个已解析请求都会回写 cookie。
|
|
173
|
+
|
|
174
|
+
```ts
|
|
175
|
+
import { createI18nMiddleware } from '@ubean/i18n';
|
|
176
|
+
|
|
177
|
+
const middleware = createI18nMiddleware({
|
|
178
|
+
defaultLocale: 'en',
|
|
179
|
+
locales: ['en', 'zh'],
|
|
180
|
+
strategy: 'prefix_except_default',
|
|
181
|
+
detectBrowserLanguage: { cookieName: 'ubean_locale', redirectOn: 'root', alwaysRedirect: false }
|
|
182
|
+
});
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
行为细节:
|
|
186
|
+
|
|
187
|
+
- 默认值:`cookieName: 'ubean_locale'`、`redirectOn: 'root'`、`alwaysRedirect: false`。
|
|
188
|
+
- `/api/`、`/_`、`/__` 路径仍进入 ALS 作用域(cookie / `Accept-Language`),但 URL 永不改写。
|
|
189
|
+
- `prefix_except_default` 会把 `/en/...` 302 到无前缀路径;`prefix` 会把无前缀内容 302 到偏好前缀,不受 `redirectOn` 影响。
|
|
190
|
+
- `redirectOn: 'root'` 只 302 `/`;`redirectOn: 'all'` 连更深路径也重定向。
|
|
191
|
+
- 中间件设置 `c.set('locale')`、`c.set('pathWithoutLocale')` 与 `Content-Language` 响应头,并追加 `Set-Cookie`。
|
|
192
|
+
|
|
193
|
+
```ts
|
|
194
|
+
import { getRequestLocale, getPathWithoutLocale } from 'ubean/i18n';
|
|
195
|
+
import { defineHandler } from 'ubean/server';
|
|
196
|
+
|
|
197
|
+
export const GET = defineHandler(c => {
|
|
198
|
+
return c.json({ locale: getRequestLocale(c), path: getPathWithoutLocale(c) });
|
|
199
|
+
});
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
中间件把解析后的语言与去前缀路径写入 context;`getPathWithoutLocale(c)` 读取后者。
|
|
203
|
+
|
|
204
|
+
## AsyncLocalStorage 中的消息
|
|
205
|
+
|
|
206
|
+
handler 侧翻译活在 ALS 作用域里。`t()` 在作用域外会**抛错** —— 不存在进程全局 locale。
|
|
207
|
+
|
|
208
|
+
```ts
|
|
209
|
+
import { t, d, n, getI18nScope } from 'ubean/i18n';
|
|
210
|
+
import { defineHandler } from 'ubean/server';
|
|
211
|
+
|
|
212
|
+
export const GET = defineHandler(c => {
|
|
213
|
+
const scope = getI18nScope();
|
|
214
|
+
return c.json({
|
|
215
|
+
hello: t('hello'),
|
|
216
|
+
greeting: t('greeting', { name: 'World' }),
|
|
217
|
+
when: d(new Date()),
|
|
218
|
+
count: n(1200)
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
语言目录与元信息 API:
|
|
224
|
+
|
|
225
|
+
```ts
|
|
226
|
+
import {
|
|
227
|
+
ensureLocaleMessages,
|
|
228
|
+
getFallbackLocale,
|
|
229
|
+
getLocaleDir,
|
|
230
|
+
getLocaleMeta,
|
|
231
|
+
registerLocaleLoader,
|
|
232
|
+
setFallbackLocale,
|
|
233
|
+
setLocaleMessages,
|
|
234
|
+
setLocaleMeta
|
|
235
|
+
} from '@ubean/i18n';
|
|
236
|
+
|
|
237
|
+
setFallbackLocale('en');
|
|
238
|
+
setLocaleMessages('en', { hello: 'Hello' });
|
|
239
|
+
|
|
240
|
+
registerLocaleLoader(async locale => {
|
|
241
|
+
const messages = await import(`../locales/${locale}.json`);
|
|
242
|
+
setLocaleMessages(locale, messages.default);
|
|
243
|
+
});
|
|
244
|
+
await ensureLocaleMessages('zh', getFallbackLocale());
|
|
245
|
+
|
|
246
|
+
setLocaleMeta('zh', { code: 'zh', language: 'zh-CN', name: '简体中文', dir: 'ltr' });
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
同样导出的还有:`getLocaleMessages` / `mergeLocaleMessages`、`listLocaleCodes`、`getRegisteredLocalesMeta`、`getLocaleName`。`createRequestContext(locale, fallback)` 构造测试与内部调用者交给 `runWithI18n(scope, fn)` 消费的作用域对象;`getAlsLocale()`(`getRequestLocale()` 在入口处的别名)无需 Hono context 即可读取当前语言。
|
|
250
|
+
|
|
251
|
+
## 语言 head(hreflang / canonical)
|
|
252
|
+
|
|
253
|
+
`buildLocaleHead` 把解析后的路由状态转成 head 标签 —— 不依赖 Vue、不依赖 unhead:
|
|
254
|
+
|
|
255
|
+
```ts
|
|
256
|
+
import { buildLocaleHead } from '@ubean/i18n';
|
|
257
|
+
|
|
258
|
+
const tags = buildLocaleHead({
|
|
259
|
+
path: '/about',
|
|
260
|
+
locale: 'zh',
|
|
261
|
+
locales: [
|
|
262
|
+
{ code: 'en', language: 'en', name: 'English', isDefault: true },
|
|
263
|
+
{ code: 'zh', language: 'zh-CN', name: '简体中文', dir: 'ltr' }
|
|
264
|
+
],
|
|
265
|
+
routing: { defaultLocale: 'en', locales: ['en', 'zh'], strategy: 'prefix_and_default' },
|
|
266
|
+
baseUrl: 'https://example.com'
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
tags.htmlAttrs; // { lang, dir }
|
|
270
|
+
tags.link; // rel=alternate hreflang 条目(含 x-default)+ canonical
|
|
271
|
+
tags.meta; // og:locale / og:locale:alternate
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
- `hreflang` 交替链接包含语言组 catch-all 条目与 `x-default`。
|
|
275
|
+
- `prefix_and_default` 下,默认语言的 canonical 指向**无前缀** URL。
|
|
276
|
+
- 设置 `baseUrl` 以输出绝对 href。
|
|
277
|
+
|
|
278
|
+
## 浏览器安全子路径
|
|
279
|
+
|
|
280
|
+
`@ubean/i18n/browser` 导出无请求依赖的子集,客户端代码因此不会拉入 Hono 与 ALS 运行时:
|
|
281
|
+
|
|
282
|
+
```ts
|
|
283
|
+
import {
|
|
284
|
+
buildLocaleHead,
|
|
285
|
+
compileLocalePaths,
|
|
286
|
+
detectLocaleFromAcceptLanguage,
|
|
287
|
+
extractLocaleFromPath,
|
|
288
|
+
getVueLocaleParam,
|
|
289
|
+
localizePath,
|
|
290
|
+
parseLocaleCookie,
|
|
291
|
+
serializeLocaleCookie,
|
|
292
|
+
switchLocalePath,
|
|
293
|
+
toVueRouterLocalePath
|
|
294
|
+
} from '@ubean/i18n/browser';
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
该入口还导出路由类型(`I18nRoutingStrategy`、`LocaleRoutingConfig`、`CompiledLocalePath`、`HonoLocalePath`、`I18nLocaleMeta`)与 head 类型(`LocaleHeadInput`、`LocaleHeadTags`)。
|
|
298
|
+
|
|
299
|
+
## 类型安全
|
|
300
|
+
|
|
301
|
+
- 路由类型:`I18nRoutingStrategy`(`'prefix' | 'prefix_except_default' | 'prefix_and_default' | 'no_prefix'`)、`LocaleRoutingConfig`、`CompiledLocalePath`、`HonoLocalePath`、`I18nMiddlewareOptions`、`I18nDetectOptions`、`I18nLocaleMeta`。
|
|
302
|
+
- 运行时类型:`LocaleMessages`、`I18nRequestScope`、`I18nCoreContext`。
|
|
303
|
+
- head 类型:`LocaleHeadInput`、`LocaleHeadTags`。
|
|
304
|
+
- `LocaleRoutingConfig` 与解析后的 `i18n` 配置结构兼容,`as const` 配置对象可直接传给纯函数工具,无需断言。
|
|
305
|
+
- 消息 key 的类型由构建流水线生成到 `.ubean/i18n.d.ts` —— 它用默认语言目录增强 vue-i18n 的 `DefineLocaleMessage`。请把 `.ubean/*.d.ts` 纳入 `tsconfig.json`。
|
|
306
|
+
|
|
307
|
+
## 架构说明
|
|
308
|
+
|
|
309
|
+
- **一个编译器,两个 router。** `compileLocalePaths` 是唯一知道前缀规则的地方;Hono 路由注册与 vue-router 路由生成都消费它的输出。改策略只改一个函数,而不是两张路由表。
|
|
310
|
+
- **无进程全局 locale。** `t()` / `d()` / `n()` 严格读 ALS;在请求作用域之外抛错,而不是静默回落到默认语言。Cloudflare Workers 需要 `nodejs_compat` 才能用 `AsyncLocalStorage`。
|
|
311
|
+
- **服务端引擎 vs Vue 引擎。** handler 用 `@intlify/core`;Vue 侧用 vue-i18n 11。两者共享语言目录,不共享实例 —— 本包从不导入 `vue-i18n`。中间件由 `createUbeanApp` 挂载,因此 `src/middleware/02.i18n.ts` 不属于本框架约定。
|
|
312
|
+
- **已移除且无兼容层:** `defineLocale`、`addLocale`、`mergeLocale`、`clearLocales`、`formatDate`、`formatNumber`、`formatCurrency`、`formatRelativeTime`、`formatList`,以及本包此前提供的进程全局 `useI18n()`。请改用 vue-i18n 的 `d` / `n` 与 ALS 的 `t()`。
|
|
313
|
+
- **决策记录:** [`docs/adr/0009-i18n-engine-and-compact-locale-routing.md`](../../docs/adr/0009-i18n-engine-and-compact-locale-routing.md)。多语言 SSG 走查示例见 [`examples/ssg-catchall`](../../examples/ssg-catchall)。
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ubean/i18n",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.4",
|
|
4
4
|
"description": "i18n for ubean (vue-i18n / @intlify/core, compact locale routing, ALS)",
|
|
5
5
|
"files": [
|
|
6
6
|
"dist"
|
|
@@ -25,13 +25,13 @@
|
|
|
25
25
|
},
|
|
26
26
|
"dependencies": {
|
|
27
27
|
"@intlify/core": "11.4.14",
|
|
28
|
-
"@ubean/shared": "0.6.
|
|
28
|
+
"@ubean/shared": "0.6.4",
|
|
29
29
|
"hono": "4.13.13"
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|
|
32
32
|
"@types/node": "^26.6.5",
|
|
33
33
|
"typescript": "npm:typescript-native-bridge@latest",
|
|
34
|
-
"vite-plus": "1.
|
|
34
|
+
"vite-plus": "1.3.0"
|
|
35
35
|
},
|
|
36
36
|
"engines": {
|
|
37
37
|
"node": ">=22"
|