@bndynet/vue-site 1.0.2 → 1.2.0
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 +229 -10
- package/README.zh.md +538 -0
- package/bin/vue-site.mjs +212 -18
- package/dist/components/LocaleSwitch.vue.d.ts +10 -0
- package/dist/composables/useLocale.d.ts +19 -0
- package/dist/composables/useLocalize.d.ts +18 -0
- package/dist/i18n-messages.d.ts +9 -0
- package/dist/i18n-utils.d.ts +81 -0
- package/dist/index.d.ts +6 -1
- package/dist/index.es.js +10993 -10562
- package/dist/router.d.ts +9 -3
- package/dist/style.css +1 -1
- package/dist/theme/presets.d.ts +7 -0
- package/dist/theme/resolve-palettes.d.ts +7 -1
- package/dist/types.d.ts +129 -17
- package/package.json +3 -2
package/bin/vue-site.mjs
CHANGED
|
@@ -5,7 +5,7 @@ import vue from '@vitejs/plugin-vue'
|
|
|
5
5
|
import { resolve, dirname, basename } from 'path'
|
|
6
6
|
import { fileURLToPath, pathToFileURL } from 'url'
|
|
7
7
|
import { createRequire } from 'module'
|
|
8
|
-
import {
|
|
8
|
+
import { build as esbuild } from 'esbuild'
|
|
9
9
|
import fs from 'fs'
|
|
10
10
|
|
|
11
11
|
const __filename = fileURLToPath(import.meta.url)
|
|
@@ -166,6 +166,29 @@ function resolveBootstrapUrl(path) {
|
|
|
166
166
|
return '/' + t.replace(/^\.\//, '')
|
|
167
167
|
}
|
|
168
168
|
|
|
169
|
+
// Friendly display names for auto-discovered locale files (`/locales/<code>.json`). Used only when
|
|
170
|
+
// the config doesn't declare `i18n.locales`; unknown codes fall back to the code itself.
|
|
171
|
+
const LOCALE_LABELS = {
|
|
172
|
+
en: 'English',
|
|
173
|
+
zh: '简体中文',
|
|
174
|
+
'zh-CN': '简体中文',
|
|
175
|
+
'zh-TW': '繁體中文',
|
|
176
|
+
ja: '日本語',
|
|
177
|
+
ko: '한국어',
|
|
178
|
+
fr: 'Français',
|
|
179
|
+
de: 'Deutsch',
|
|
180
|
+
es: 'Español',
|
|
181
|
+
pt: 'Português',
|
|
182
|
+
ru: 'Русский',
|
|
183
|
+
it: 'Italiano',
|
|
184
|
+
nl: 'Nederlands',
|
|
185
|
+
pl: 'Polski',
|
|
186
|
+
tr: 'Türkçe',
|
|
187
|
+
vi: 'Tiếng Việt',
|
|
188
|
+
th: 'ไทย',
|
|
189
|
+
ar: 'العربية',
|
|
190
|
+
}
|
|
191
|
+
|
|
169
192
|
/**
|
|
170
193
|
* Bootstrap script shared by dev (virtual entry) and build (inlined in html).
|
|
171
194
|
* `siteConfigSpecifier` differs because dev serves from Vite root (`/foo`)
|
|
@@ -173,6 +196,11 @@ function resolveBootstrapUrl(path) {
|
|
|
173
196
|
*
|
|
174
197
|
* Static import bundles `bootstrap` for production; dynamic import with
|
|
175
198
|
* vite-ignore is not emitted.
|
|
199
|
+
*
|
|
200
|
+
* Convention: translations are auto-loaded from `/locales/<code>.json` (relative to the Vite root,
|
|
201
|
+
* i.e. the config's directory). The user writes zero glue code — no `index.ts`, no `messages` field.
|
|
202
|
+
* An explicit `i18n.messages` still works and overrides auto-loaded keys; an explicit `i18n.locales`
|
|
203
|
+
* still controls the label/icon/order, otherwise the locale list is derived from the file names.
|
|
176
204
|
*/
|
|
177
205
|
function buildBootstrapScript({ siteConfig, siteConfigSpecifier }) {
|
|
178
206
|
const bs = siteConfig?.bootstrap
|
|
@@ -189,6 +217,43 @@ function buildBootstrapScript({ siteConfig, siteConfigSpecifier }) {
|
|
|
189
217
|
`import '${pkgDirUrl}/dist/style.css'`,
|
|
190
218
|
`import siteConfig from '${siteConfigSpecifier}'`,
|
|
191
219
|
`import { repositoryUrl } from '${VIRTUAL_PACKAGE}'`,
|
|
220
|
+
``,
|
|
221
|
+
`// Auto-discover translations: /locales/<code>.json -> { [code]: { ...messages } }.`,
|
|
222
|
+
`const __localeFiles = import.meta.glob('/locales/*.json', { eager: true, import: 'default' })`,
|
|
223
|
+
`const __LOCALE_LABELS = ${JSON.stringify(LOCALE_LABELS)}`,
|
|
224
|
+
`const __autoMessages = {}`,
|
|
225
|
+
`for (const __p in __localeFiles) {`,
|
|
226
|
+
` const __code = __p.slice(__p.lastIndexOf('/') + 1).replace(/\\.json$/, '')`,
|
|
227
|
+
` __autoMessages[__code] = __localeFiles[__p]`,
|
|
228
|
+
`}`,
|
|
229
|
+
`const __autoCodes = Object.keys(__autoMessages).sort()`,
|
|
230
|
+
`function __deepMerge(base, override) {`,
|
|
231
|
+
` const out = { ...base }`,
|
|
232
|
+
` for (const k in (override || {})) {`,
|
|
233
|
+
` const a = out[k], b = override[k]`,
|
|
234
|
+
` out[k] = a && b && typeof a === 'object' && typeof b === 'object' && !Array.isArray(a) && !Array.isArray(b)`,
|
|
235
|
+
` ? __deepMerge(a, b) : b`,
|
|
236
|
+
` }`,
|
|
237
|
+
` return out`,
|
|
238
|
+
`}`,
|
|
239
|
+
`function __mergeMessages(base, override) {`,
|
|
240
|
+
` const out = {}`,
|
|
241
|
+
` const keys = new Set([...Object.keys(base), ...Object.keys(override || {})])`,
|
|
242
|
+
` for (const k of keys) out[k] = __deepMerge(base[k] || {}, (override || {})[k] || {})`,
|
|
243
|
+
` return out`,
|
|
244
|
+
`}`,
|
|
245
|
+
`// Merge auto-loaded files into i18n. Explicit config wins: messages override per key, and an`,
|
|
246
|
+
`// explicit locales list controls label/icon/order (else it's derived from the file names).`,
|
|
247
|
+
`function __resolveI18n(cfg) {`,
|
|
248
|
+
` const hasAuto = __autoCodes.length > 0`,
|
|
249
|
+
` if (!cfg && !hasAuto) return cfg`,
|
|
250
|
+
` const base = cfg || {}`,
|
|
251
|
+
` let locales = base.locales`,
|
|
252
|
+
` if ((!locales || !locales.length) && hasAuto) {`,
|
|
253
|
+
` locales = __autoCodes.map((c) => ({ code: c, label: __LOCALE_LABELS[c] || c }))`,
|
|
254
|
+
` }`,
|
|
255
|
+
` return { ...base, locales, messages: __mergeMessages(__autoMessages, base.messages) }`,
|
|
256
|
+
`}`,
|
|
192
257
|
`;(async () => {`,
|
|
193
258
|
` const searchParams = new URLSearchParams(window.location.search)`,
|
|
194
259
|
` const hasThemeQuery = searchParams.has('theme')`,
|
|
@@ -203,6 +268,7 @@ function buildBootstrapScript({ siteConfig, siteConfigSpecifier }) {
|
|
|
203
268
|
` }`,
|
|
204
269
|
` const app = await createSiteApp({`,
|
|
205
270
|
` ...siteConfig,`,
|
|
271
|
+
` i18n: __resolveI18n(siteConfig.i18n),`,
|
|
206
272
|
` ...(hasThemeQuery ? { theme: { ...(siteConfig.theme || {}), default: resolvedTheme } } : {}),`,
|
|
207
273
|
` packageRepository: repositoryUrl,`,
|
|
208
274
|
` baseUrl: import.meta.env.BASE_URL,`,
|
|
@@ -238,39 +304,167 @@ const htmlTemplate = buildHtmlShell(
|
|
|
238
304
|
`<script type="module" src="/@id/__x00__${VIRTUAL_ENTRY}"></script>`,
|
|
239
305
|
)
|
|
240
306
|
|
|
307
|
+
// esbuild plugin stubbing asset / SFC / `?raw` imports (static or dynamic) to an empty default
|
|
308
|
+
// export, so bundling the config for pre-load doesn't choke on resources Node can't load. Page
|
|
309
|
+
// loaders are never invoked during pre-load (only build-time settings are read).
|
|
310
|
+
function preloadAssetStubPlugin() {
|
|
311
|
+
const NS = 'vue-site-asset'
|
|
312
|
+
const ASSET =
|
|
313
|
+
/\?raw(?:&\S*)?$|\.(?:vue|css|scss|sass|less|styl|md|markdown|png|jpe?g|gif|svg|webp|avif|ico)(?:\?\S*)?$/
|
|
314
|
+
return {
|
|
315
|
+
name: 'vue-site:preload-asset-stub',
|
|
316
|
+
setup(b) {
|
|
317
|
+
b.onResolve({ filter: ASSET }, (args) => ({ path: args.path, namespace: NS }))
|
|
318
|
+
b.onLoad({ filter: /.*/, namespace: NS }, () => ({
|
|
319
|
+
contents: 'export default ""',
|
|
320
|
+
loader: 'js',
|
|
321
|
+
}))
|
|
322
|
+
},
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
241
326
|
async function loadSiteConfig() {
|
|
242
327
|
const configPath = resolve(cwd, foundConfig)
|
|
243
328
|
const raw = fs.readFileSync(configPath, 'utf-8')
|
|
244
329
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
330
|
+
// Stub the framework's value imports so the config evaluates without the real (browser-only)
|
|
331
|
+
// package. `defineConfig` is identity (the config object passes through); every other named
|
|
332
|
+
// import (e.g. `tk`, `localizedPage`) becomes a callable no-op that returns a no-op, covering
|
|
333
|
+
// helpers used as values. Relative imports (e.g. `./locales`) are left intact and bundled below.
|
|
334
|
+
let stubbed = raw.replace(
|
|
335
|
+
/import\s*\{([^}]*)\}\s*from\s*['"][^'"]*vue-site['"]\s*;?/g,
|
|
336
|
+
(_match, names) => {
|
|
337
|
+
const ids = names
|
|
338
|
+
.split(',')
|
|
339
|
+
.map((part) => part.trim())
|
|
340
|
+
.filter(Boolean)
|
|
341
|
+
.map((part) => {
|
|
342
|
+
const segments = part.split(/\s+as\s+/)
|
|
343
|
+
return (segments[1] ?? segments[0]).trim()
|
|
344
|
+
})
|
|
345
|
+
.filter(Boolean)
|
|
346
|
+
return ids
|
|
347
|
+
.map((id) =>
|
|
348
|
+
id === 'defineConfig'
|
|
349
|
+
? 'const defineConfig = (c) => c;'
|
|
350
|
+
: `const ${id} = (..._args) => (() => {});`,
|
|
351
|
+
)
|
|
352
|
+
.join('\n')
|
|
353
|
+
},
|
|
254
354
|
)
|
|
255
355
|
|
|
256
|
-
|
|
257
|
-
|
|
356
|
+
// `import.meta.glob(...)` is a Vite-only feature; in Node it would throw at config-eval time.
|
|
357
|
+
// Stub it to an empty map so configs using `localizedPageGlob(import.meta.glob(...))` pre-load
|
|
358
|
+
// (page loaders are never invoked here — only build-time settings are read). The real glob runs
|
|
359
|
+
// in the browser via Vite.
|
|
360
|
+
if (/import\.meta\.glob/.test(stubbed)) {
|
|
361
|
+
stubbed =
|
|
362
|
+
'const __vueSiteGlobStub = (..._args) => ({});\n' +
|
|
363
|
+
stubbed.replace(/import\.meta\.glob/g, '__vueSiteGlobStub')
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
const isTs = /\.m?ts$/.test(foundConfig)
|
|
367
|
+
// Write the stubbed entry next to the original so its relative imports (`./locales`) resolve.
|
|
368
|
+
const entryFile = resolve(
|
|
369
|
+
dirname(configPath),
|
|
370
|
+
`.${basename(configPath)}.${Date.now()}.preload.${isTs ? 'ts' : 'js'}`,
|
|
371
|
+
)
|
|
372
|
+
const tmpDir = resolve(cwd, `.vue-site-preload-${Date.now()}`)
|
|
373
|
+
fs.writeFileSync(entryFile, stubbed)
|
|
258
374
|
|
|
259
375
|
try {
|
|
260
|
-
|
|
376
|
+
// Bundle so local modules the config imports (e.g. `./locales.ts`) are inlined and TS is
|
|
377
|
+
// handled; asset imports are stubbed; remaining bare deps stay external for Node to resolve.
|
|
378
|
+
await esbuild({
|
|
379
|
+
entryPoints: { 'site-config': entryFile },
|
|
380
|
+
outdir: tmpDir,
|
|
381
|
+
bundle: true,
|
|
382
|
+
format: 'esm',
|
|
383
|
+
platform: 'node',
|
|
384
|
+
splitting: true,
|
|
385
|
+
logLevel: 'silent',
|
|
386
|
+
packages: 'external',
|
|
387
|
+
outExtension: { '.js': '.mjs' },
|
|
388
|
+
plugins: [preloadAssetStubPlugin()],
|
|
389
|
+
})
|
|
390
|
+
|
|
391
|
+
const entry = resolve(tmpDir, 'site-config.mjs')
|
|
392
|
+
const mod = await import(pathToFileURL(entry).href)
|
|
261
393
|
return mod.default || {}
|
|
262
394
|
} catch (e) {
|
|
263
|
-
|
|
264
|
-
`[vue-site] Could not pre-load site config from ${foundConfig}: ${e.message}
|
|
395
|
+
throw new Error(
|
|
396
|
+
`[vue-site] Could not pre-load site config from ${foundConfig}: ${e.message}\n` +
|
|
397
|
+
` This usually means your config imports modules Node can't resolve directly ` +
|
|
398
|
+
`(path aliases like @/..., or framework APIs other than defineConfig).`,
|
|
265
399
|
)
|
|
266
|
-
return {}
|
|
267
400
|
} finally {
|
|
268
401
|
try {
|
|
269
|
-
fs.unlinkSync(
|
|
402
|
+
fs.unlinkSync(entryFile)
|
|
403
|
+
} catch {}
|
|
404
|
+
try {
|
|
405
|
+
fs.rmSync(tmpDir, { recursive: true, force: true })
|
|
270
406
|
} catch {}
|
|
271
407
|
}
|
|
272
408
|
}
|
|
273
409
|
|
|
410
|
+
/**
|
|
411
|
+
* Turn a base file path into a `import.meta.glob(...)` expression matching the base file plus its
|
|
412
|
+
* per-locale siblings (`name.<code>.ext`) — but NOT unrelated `nameOther.ext`:
|
|
413
|
+
* `../README.md` -> import.meta.glob(["../README.md","../README.*.md"], { query: '?raw' })
|
|
414
|
+
* `./pages/Home.vue` -> import.meta.glob(["./pages/Home.vue","./pages/Home.*.vue"], {})
|
|
415
|
+
* Markdown is loaded `?raw` (string content); other files (e.g. `.vue`) export a component.
|
|
416
|
+
* Returns `null` when the path has no extension (can't build a sensible glob).
|
|
417
|
+
*/
|
|
418
|
+
function fileToLocaleGlobExpr(rawPath) {
|
|
419
|
+
const qIdx = rawPath.indexOf('?')
|
|
420
|
+
const query = qIdx >= 0 ? rawPath.slice(qIdx + 1) : ''
|
|
421
|
+
const pathOnly = qIdx >= 0 ? rawPath.slice(0, qIdx) : rawPath
|
|
422
|
+
const dot = pathOnly.lastIndexOf('.')
|
|
423
|
+
if (dot <= pathOnly.lastIndexOf('/')) return null
|
|
424
|
+
const ext = pathOnly.slice(dot)
|
|
425
|
+
const globs = [pathOnly, `${pathOnly.slice(0, dot)}.*${ext}`]
|
|
426
|
+
const isMarkdown = /\.(?:md|markdown)$/i.test(ext) || /(?:^|&)raw(?:$|&)/.test(query)
|
|
427
|
+
const opts = isMarkdown ? `{ query: '?raw' }` : `{}`
|
|
428
|
+
return `import.meta.glob(${JSON.stringify(globs)}, ${opts})`
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// Sugar so configs can name page files as plain strings; the CLI turns them into Vite globs so the
|
|
432
|
+
// per-locale files get bundled. Two shapes are rewritten in the user's JS/TS under `cwd`:
|
|
433
|
+
// localizedPage('./file.md') -> localizedPage(import.meta.glob([...], { query: '?raw' }))
|
|
434
|
+
// page: './file.md' -> page: import.meta.glob([...], { query: '?raw' })
|
|
435
|
+
// Loader functions, locale maps (`localizedPage({ en, zh })`) and explicit `import.meta.glob` are
|
|
436
|
+
// left untouched; the `page:` form only rewrites path-like values (starting with `.` or `/`).
|
|
437
|
+
function localizedPageSugarPlugin() {
|
|
438
|
+
const CALL_STRING_ARG = /(\blocalizedPage\s*\(\s*)(['"`])((?:\\.|(?!\2).)*)\2/g
|
|
439
|
+
const PAGE_STRING_FIELD = /(\bpage\s*:\s*)(['"`])((?:\\.|(?!\2).)*)\2/g
|
|
440
|
+
return {
|
|
441
|
+
name: 'vue-site:localized-page-sugar',
|
|
442
|
+
enforce: 'pre',
|
|
443
|
+
transform(code, id) {
|
|
444
|
+
const file = id.split('?')[0]
|
|
445
|
+
if (!/\.(?:[cm]?[jt]sx?)$/.test(file)) return
|
|
446
|
+
if (!file.startsWith(cwd) || file.includes('/node_modules/')) return
|
|
447
|
+
if (!code.includes('localizedPage(') && !/\bpage\s*:\s*['"`]/.test(code)) return
|
|
448
|
+
let changed = false
|
|
449
|
+
let out = code.replace(CALL_STRING_ARG, (match, head, _q, rawPath) => {
|
|
450
|
+
const expr = fileToLocaleGlobExpr(rawPath)
|
|
451
|
+
if (!expr) return match
|
|
452
|
+
changed = true
|
|
453
|
+
return `${head}${expr}`
|
|
454
|
+
})
|
|
455
|
+
out = out.replace(PAGE_STRING_FIELD, (match, head, _q, rawPath) => {
|
|
456
|
+
// Only rewrite path-like values to avoid touching unrelated `page: '...'` properties.
|
|
457
|
+
if (!/^[./]/.test(rawPath)) return match
|
|
458
|
+
const expr = fileToLocaleGlobExpr(rawPath)
|
|
459
|
+
if (!expr) return match
|
|
460
|
+
changed = true
|
|
461
|
+
return `${head}${expr}`
|
|
462
|
+
})
|
|
463
|
+
return changed ? { code: out, map: null } : undefined
|
|
464
|
+
},
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
274
468
|
function vueSitePlugin(entryCode) {
|
|
275
469
|
return [
|
|
276
470
|
{
|
|
@@ -458,7 +652,7 @@ async function buildViteConfig(options = {}) {
|
|
|
458
652
|
|
|
459
653
|
const baseConfig = {
|
|
460
654
|
root: cwd,
|
|
461
|
-
plugins: [vue(vueOpts), ...watchedScssPlugin, ...vueSitePlugin(entryCode), ...(userPlugins || [])],
|
|
655
|
+
plugins: [localizedPageSugarPlugin(), vue(vueOpts), ...watchedScssPlugin, ...vueSitePlugin(entryCode), ...(userPlugins || [])],
|
|
462
656
|
resolve: {
|
|
463
657
|
alias: {
|
|
464
658
|
vue: resolve(vuePath, 'dist/vue.runtime.esm-bundler.js'),
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
type __VLS_Props = {
|
|
2
|
+
compact?: boolean;
|
|
3
|
+
};
|
|
4
|
+
declare const _default: import('vue').DefineComponent<__VLS_Props, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {}, string, import('vue').PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {
|
|
5
|
+
compact: boolean;
|
|
6
|
+
}, {}, {}, {}, string, import('vue').ComponentProvideOptions, false, {
|
|
7
|
+
detailsRef: HTMLDetailsElement;
|
|
8
|
+
summaryRef: HTMLElement;
|
|
9
|
+
}, HTMLDetailsElement>;
|
|
10
|
+
export default _default;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { InjectionKey, Ref } from 'vue';
|
|
2
|
+
/** Use `Symbol.for` so the key matches even if multiple copies of this package are resolved. */
|
|
3
|
+
export declare const localeRefKey: InjectionKey<Ref<string>>;
|
|
4
|
+
/**
|
|
5
|
+
* Resolve and apply the initial locale (stored > detected > default), then keep `localeRef`
|
|
6
|
+
* authoritative. Call once from `createSiteApp` when `i18n` is configured.
|
|
7
|
+
*
|
|
8
|
+
* @param localeRef — ref created next to `createApp` so it uses the same Vue runtime as the app.
|
|
9
|
+
* @param defaultLocale — used when nothing valid is in localStorage and detection finds no match.
|
|
10
|
+
* @param locales — full list of allowed locale codes.
|
|
11
|
+
* @param detectBrowser — try `navigator.language(s)` before falling back to `defaultLocale`.
|
|
12
|
+
* @param key — localStorage key for persistence (default `vue-site-locale`).
|
|
13
|
+
*/
|
|
14
|
+
export declare function initLocale(localeRef: Ref<string>, defaultLocale: string, locales: readonly string[], detectBrowser?: boolean, key?: string): void;
|
|
15
|
+
export declare function useLocale(): {
|
|
16
|
+
locale: Ref<string, string>;
|
|
17
|
+
setLocale: (code: string) => void;
|
|
18
|
+
locales: () => string[];
|
|
19
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { LocalizedString } from '../types';
|
|
2
|
+
/**
|
|
3
|
+
* Reactively resolve localized content against the active locale. Returns:
|
|
4
|
+
*
|
|
5
|
+
* - `localize(value)` — resolve a `LocalizedString` (locale map, plain string, or a `tk()` key
|
|
6
|
+
* reference); plain strings pass through unchanged.
|
|
7
|
+
* - `t(id, params?)` — resolve a UI message id from `SiteConfig.i18n.messages` layered over the
|
|
8
|
+
* framework's built-in strings, with locale fallback (exact → primary-subtag → default → `en`)
|
|
9
|
+
* and `{name}` placeholder interpolation.
|
|
10
|
+
* - `locale` — the active locale ref.
|
|
11
|
+
*
|
|
12
|
+
* Reading these inside a template/computed makes the output update automatically on language change.
|
|
13
|
+
*/
|
|
14
|
+
export declare function useLocalize(): {
|
|
15
|
+
localize: (value: LocalizedString | undefined) => string;
|
|
16
|
+
t: (id: string, params?: Record<string, string | number>) => string;
|
|
17
|
+
locale: import('vue').Ref<string, string>;
|
|
18
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { LocaleCode } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* Built-in UI strings for framework chrome (theme/locale switchers, page errors). Keyed by locale
|
|
4
|
+
* then message id. Consumers override or extend these per locale via `SiteConfig.i18n.messages`,
|
|
5
|
+
* which is merged on top of these defaults. Unknown locales fall back to `en`.
|
|
6
|
+
*
|
|
7
|
+
* Body strings may contain `{name}` placeholders, interpolated by `useLocalize().t(id, params)`.
|
|
8
|
+
*/
|
|
9
|
+
export declare const builtinMessages: Record<LocaleCode, Record<string, string>>;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { Component } from 'vue';
|
|
2
|
+
import { LocaleCode, LocalizedString, MessageRef, MessageTree, PageLoader } from './types';
|
|
3
|
+
/** Merged, flattened message dictionaries keyed by locale then dotted message id. */
|
|
4
|
+
export type MessageCatalog = Record<LocaleCode, Record<string, string>>;
|
|
5
|
+
/**
|
|
6
|
+
* Flatten a (possibly nested) message tree to dotted ids: `{ site: { title: 'x' } }` → `site.title`.
|
|
7
|
+
* Flat dictionaries pass through unchanged, so both layouts can be mixed.
|
|
8
|
+
*/
|
|
9
|
+
export declare function flattenMessages(tree: MessageTree | undefined, prefix?: string): Record<string, string>;
|
|
10
|
+
/** Type guard for a {@link MessageRef} (a `{ $t }` key reference). */
|
|
11
|
+
export declare function isMessageRef(value: unknown): value is MessageRef;
|
|
12
|
+
/** Build a `MessageRef` referencing a central message id; use in `LocalizedString` config fields. */
|
|
13
|
+
export declare function tk(id: string, params?: Record<string, string | number>): MessageRef;
|
|
14
|
+
/**
|
|
15
|
+
* Merge `override` message trees on top of `base`, per locale, flattening nested groups to dotted
|
|
16
|
+
* ids. Returns a flat {@link MessageCatalog} ready for {@link resolveMessage}.
|
|
17
|
+
*/
|
|
18
|
+
export declare function mergeCatalog(base: Record<LocaleCode, MessageTree>, override?: Record<LocaleCode, MessageTree>): MessageCatalog;
|
|
19
|
+
/**
|
|
20
|
+
* Resolve a message id from a merged `catalog` for the active locale, with fallback
|
|
21
|
+
* (exact → primary-subtag → `defaultLocale` → `en` → the id itself) and `{name}` interpolation.
|
|
22
|
+
*/
|
|
23
|
+
export declare function resolveMessage(catalog: MessageCatalog, id: string, locale: string, defaultLocale?: LocaleCode, params?: Record<string, string | number>): string;
|
|
24
|
+
/**
|
|
25
|
+
* Resolve a `LocalizedString` to a plain string for the active `locale`.
|
|
26
|
+
*
|
|
27
|
+
* A bare `string` is returned unchanged. For a locale map, resolution order is:
|
|
28
|
+
* exact locale → primary-subtag match (e.g. `en-US` → `en`) → `defaultLocale` → first entry → `''`.
|
|
29
|
+
* A {@link MessageRef} is returned as its raw id here (use {@link resolveField} with a catalog to
|
|
30
|
+
* resolve it). This keeps single-language configs (plain strings) working without locale context.
|
|
31
|
+
*/
|
|
32
|
+
export declare function resolveLocalized(value: LocalizedString | undefined, locale: string, defaultLocale?: LocaleCode): string;
|
|
33
|
+
/**
|
|
34
|
+
* Resolve any `LocalizedString` — including a {@link MessageRef} — against the active locale.
|
|
35
|
+
* Plain strings and locale maps go through {@link resolveLocalized}; key references are resolved
|
|
36
|
+
* from the merged message `catalog` via {@link resolveMessage}.
|
|
37
|
+
*/
|
|
38
|
+
export declare function resolveField(value: LocalizedString | undefined, locale: string, defaultLocale?: LocaleCode, catalog?: MessageCatalog): string;
|
|
39
|
+
/** Options for {@link localizedPage}. */
|
|
40
|
+
export interface LocalizedPageOptions {
|
|
41
|
+
/** Locale to fall back to when the active locale has no entry (else the first entry is used). */
|
|
42
|
+
defaultLocale?: LocaleCode;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Build a per-locale page loader for `NavItem.page` / `StandalonePage.page`. The returned loader
|
|
46
|
+
* receives the active locale and resolves the matching content, with graceful fallback. Three forms:
|
|
47
|
+
*
|
|
48
|
+
* - **File name** (recommended, simplest) — `localizedPage('../README.md')`. The **vue-site CLI**
|
|
49
|
+
* rewrites this to a glob, so every `README.<code>.md` sitting next to the base file is picked up
|
|
50
|
+
* automatically (e.g. `README.zh.md` → `zh`); a locale with no file falls back to the base file
|
|
51
|
+
* (`README.md`). Add a language by dropping in a file — no config edits. Works for Markdown
|
|
52
|
+
* (`.md`, loaded as `?raw`) and Vue pages (`.vue`). _Only the CLI understands this form; in
|
|
53
|
+
* library mode use the glob form below._
|
|
54
|
+
* - **Glob map** — `localizedPage(import.meta.glob('../README*.md', { query: '?raw' }))`. Same
|
|
55
|
+
* behavior as the file-name form, written explicitly (use this in library mode). Pass a **lazy**
|
|
56
|
+
* glob whose modules expose `{ default }`.
|
|
57
|
+
* - **Locale map** — `localizedPage({ en: () => import('...'), zh: () => import('...') })` for files
|
|
58
|
+
* that don't share a base name.
|
|
59
|
+
*
|
|
60
|
+
* @example
|
|
61
|
+
* // Simplest (via the CLI): ../README.md (base) + ../README.zh.md + ../README.ja.md + ...
|
|
62
|
+
* page: localizedPage('../README.md')
|
|
63
|
+
*
|
|
64
|
+
* @example
|
|
65
|
+
* // Explicit locale map
|
|
66
|
+
* page: localizedPage({
|
|
67
|
+
* en: () => import('./pages/guide.en.md?raw'),
|
|
68
|
+
* zh: () => import('./pages/guide.zh.md?raw'),
|
|
69
|
+
* })
|
|
70
|
+
*/
|
|
71
|
+
export declare function localizedPage(file: string, options?: LocalizedPageOptions): PageLoader;
|
|
72
|
+
export declare function localizedPage(loaders: Record<LocaleCode, () => Promise<{
|
|
73
|
+
default: string;
|
|
74
|
+
}>>, options?: LocalizedPageOptions): (locale: LocaleCode) => Promise<{
|
|
75
|
+
default: string;
|
|
76
|
+
}>;
|
|
77
|
+
export declare function localizedPage(loaders: Record<LocaleCode, () => Promise<{
|
|
78
|
+
default: Component;
|
|
79
|
+
}>>, options?: LocalizedPageOptions): (locale: LocaleCode) => Promise<{
|
|
80
|
+
default: Component;
|
|
81
|
+
}>;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
import { SiteConfig } from './types';
|
|
2
2
|
export { createSiteApp } from './create-app';
|
|
3
3
|
export { useTheme, themeRefKey } from './composables/useTheme';
|
|
4
|
+
export { useLocale, localeRefKey } from './composables/useLocale';
|
|
5
|
+
export { useLocalize } from './composables/useLocalize';
|
|
6
|
+
export { resolveLocalized, resolveField, resolveMessage, mergeCatalog, flattenMessages, tk, isMessageRef, localizedPage } from './i18n-utils';
|
|
7
|
+
export type { LocalizedPageOptions, MessageCatalog } from './i18n-utils';
|
|
8
|
+
export { builtinMessages } from './i18n-messages';
|
|
4
9
|
export { useSiteConfig } from './composables/useSiteConfig';
|
|
5
10
|
export { builtinThemePalettes } from './theme/presets';
|
|
6
11
|
export { ElMessage, ElMessageBox, ElNotification, } from 'element-plus';
|
|
7
|
-
export type { SiteConfig, SiteEnvConfig, SiteViteConfig, SiteExternalLink, NavItem, StandalonePage, AuthRule, AuthContext, AuthConfig, RouterConfig, ThemeConfig, ThemeOption, ThemePaletteVars, ResolvedNavItem, } from './types';
|
|
12
|
+
export type { SiteConfig, SiteEnvConfig, SiteViteConfig, SiteExternalLink, NavItem, StandalonePage, AuthRule, AuthContext, AuthConfig, RouterConfig, ThemeConfig, ThemeOption, ThemePaletteVars, ResolvedNavItem, LocaleCode, LocalizedString, MessageRef, MessageTree, LocaleOption, I18nConfig, PageLoader, } from './types';
|
|
8
13
|
export declare function defineConfig(config: SiteConfig): SiteConfig;
|