@bndynet/vue-site 1.0.4 → 1.2.1

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/bin/vue-site.mjs CHANGED
@@ -5,13 +5,16 @@ 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 { transform } from 'esbuild'
8
+ import { build as esbuild } from 'esbuild'
9
9
  import fs from 'fs'
10
10
 
11
11
  const __filename = fileURLToPath(import.meta.url)
12
12
  const __dirname = dirname(__filename)
13
13
  const pkgDir = resolve(__dirname, '..')
14
14
  const require = createRequire(import.meta.url)
15
+ const FRAMEWORK_PACKAGE = '@bndynet/vue-site'
16
+ const frameworkEntry = resolve(pkgDir, 'dist/index.es.js')
17
+ const frameworkStyle = resolve(pkgDir, 'dist/style.css')
15
18
 
16
19
  function resolvePkgDir(pkg) {
17
20
  return dirname(require.resolve(`${pkg}/package.json`))
@@ -166,6 +169,29 @@ function resolveBootstrapUrl(path) {
166
169
  return '/' + t.replace(/^\.\//, '')
167
170
  }
168
171
 
172
+ // Friendly display names for auto-discovered locale files (`/locales/<code>.json`). Used only when
173
+ // the config doesn't declare `i18n.locales`; unknown codes fall back to the code itself.
174
+ const LOCALE_LABELS = {
175
+ en: 'English',
176
+ zh: '简体中文',
177
+ 'zh-CN': '简体中文',
178
+ 'zh-TW': '繁體中文',
179
+ ja: '日本語',
180
+ ko: '한국어',
181
+ fr: 'Français',
182
+ de: 'Deutsch',
183
+ es: 'Español',
184
+ pt: 'Português',
185
+ ru: 'Русский',
186
+ it: 'Italiano',
187
+ nl: 'Nederlands',
188
+ pl: 'Polski',
189
+ tr: 'Türkçe',
190
+ vi: 'Tiếng Việt',
191
+ th: 'ไทย',
192
+ ar: 'العربية',
193
+ }
194
+
169
195
  /**
170
196
  * Bootstrap script shared by dev (virtual entry) and build (inlined in html).
171
197
  * `siteConfigSpecifier` differs because dev serves from Vite root (`/foo`)
@@ -173,6 +199,11 @@ function resolveBootstrapUrl(path) {
173
199
  *
174
200
  * Static import bundles `bootstrap` for production; dynamic import with
175
201
  * vite-ignore is not emitted.
202
+ *
203
+ * Convention: translations are auto-loaded from `/locales/<code>.json` (relative to the Vite root,
204
+ * i.e. the config's directory). The user writes zero glue code — no `index.ts`, no `messages` field.
205
+ * An explicit `i18n.messages` still works and overrides auto-loaded keys; an explicit `i18n.locales`
206
+ * still controls the label/icon/order, otherwise the locale list is derived from the file names.
176
207
  */
177
208
  function buildBootstrapScript({ siteConfig, siteConfigSpecifier }) {
178
209
  const bs = siteConfig?.bootstrap
@@ -180,15 +211,51 @@ function buildBootstrapScript({ siteConfig, siteConfigSpecifier }) {
180
211
  bs != null && String(bs).trim() !== ''
181
212
  ? `import '${resolveBootstrapUrl(bs)}'\n`
182
213
  : ''
183
- const pkgDirUrl = pkgDir.replace(/\\/g, '/')
184
214
  return [
185
215
  bootstrapImport,
186
216
  `import 'element-plus/dist/index.css'`,
187
217
  `import 'element-plus/theme-chalk/dark/css-vars.css'`,
188
- `import { createSiteApp } from '${pkgDirUrl}/dist/index.es.js'`,
189
- `import '${pkgDirUrl}/dist/style.css'`,
218
+ `import { createSiteApp } from '${FRAMEWORK_PACKAGE}'`,
219
+ `import '${FRAMEWORK_PACKAGE}/style.css'`,
190
220
  `import siteConfig from '${siteConfigSpecifier}'`,
191
221
  `import { repositoryUrl } from '${VIRTUAL_PACKAGE}'`,
222
+ ``,
223
+ `// Auto-discover translations: /locales/<code>.json -> { [code]: { ...messages } }.`,
224
+ `const __localeFiles = import.meta.glob('/locales/*.json', { eager: true, import: 'default' })`,
225
+ `const __LOCALE_LABELS = ${JSON.stringify(LOCALE_LABELS)}`,
226
+ `const __autoMessages = {}`,
227
+ `for (const __p in __localeFiles) {`,
228
+ ` const __code = __p.slice(__p.lastIndexOf('/') + 1).replace(/\\.json$/, '')`,
229
+ ` __autoMessages[__code] = __localeFiles[__p]`,
230
+ `}`,
231
+ `const __autoCodes = Object.keys(__autoMessages).sort()`,
232
+ `function __deepMerge(base, override) {`,
233
+ ` const out = { ...base }`,
234
+ ` for (const k in (override || {})) {`,
235
+ ` const a = out[k], b = override[k]`,
236
+ ` out[k] = a && b && typeof a === 'object' && typeof b === 'object' && !Array.isArray(a) && !Array.isArray(b)`,
237
+ ` ? __deepMerge(a, b) : b`,
238
+ ` }`,
239
+ ` return out`,
240
+ `}`,
241
+ `function __mergeMessages(base, override) {`,
242
+ ` const out = {}`,
243
+ ` const keys = new Set([...Object.keys(base), ...Object.keys(override || {})])`,
244
+ ` for (const k of keys) out[k] = __deepMerge(base[k] || {}, (override || {})[k] || {})`,
245
+ ` return out`,
246
+ `}`,
247
+ `// Merge auto-loaded files into i18n. Explicit config wins: messages override per key, and an`,
248
+ `// explicit locales list controls label/icon/order (else it's derived from the file names).`,
249
+ `function __resolveI18n(cfg) {`,
250
+ ` const hasAuto = __autoCodes.length > 0`,
251
+ ` if (!cfg && !hasAuto) return cfg`,
252
+ ` const base = cfg || {}`,
253
+ ` let locales = base.locales`,
254
+ ` if ((!locales || !locales.length) && hasAuto) {`,
255
+ ` locales = __autoCodes.map((c) => ({ code: c, label: __LOCALE_LABELS[c] || c }))`,
256
+ ` }`,
257
+ ` return { ...base, locales, messages: __mergeMessages(__autoMessages, base.messages) }`,
258
+ `}`,
192
259
  `;(async () => {`,
193
260
  ` const searchParams = new URLSearchParams(window.location.search)`,
194
261
  ` const hasThemeQuery = searchParams.has('theme')`,
@@ -203,6 +270,7 @@ function buildBootstrapScript({ siteConfig, siteConfigSpecifier }) {
203
270
  ` }`,
204
271
  ` const app = await createSiteApp({`,
205
272
  ` ...siteConfig,`,
273
+ ` i18n: __resolveI18n(siteConfig.i18n),`,
206
274
  ` ...(hasThemeQuery ? { theme: { ...(siteConfig.theme || {}), default: resolvedTheme } } : {}),`,
207
275
  ` packageRepository: repositoryUrl,`,
208
276
  ` baseUrl: import.meta.env.BASE_URL,`,
@@ -238,40 +306,167 @@ const htmlTemplate = buildHtmlShell(
238
306
  `<script type="module" src="/@id/__x00__${VIRTUAL_ENTRY}"></script>`,
239
307
  )
240
308
 
309
+ // esbuild plugin stubbing asset / SFC / `?raw` imports (static or dynamic) to an empty default
310
+ // export, so bundling the config for pre-load doesn't choke on resources Node can't load. Page
311
+ // loaders are never invoked during pre-load (only build-time settings are read).
312
+ function preloadAssetStubPlugin() {
313
+ const NS = 'vue-site-asset'
314
+ const ASSET =
315
+ /\?raw(?:&\S*)?$|\.(?:vue|css|scss|sass|less|styl|md|markdown|png|jpe?g|gif|svg|webp|avif|ico)(?:\?\S*)?$/
316
+ return {
317
+ name: 'vue-site:preload-asset-stub',
318
+ setup(b) {
319
+ b.onResolve({ filter: ASSET }, (args) => ({ path: args.path, namespace: NS }))
320
+ b.onLoad({ filter: /.*/, namespace: NS }, () => ({
321
+ contents: 'export default ""',
322
+ loader: 'js',
323
+ }))
324
+ },
325
+ }
326
+ }
327
+
241
328
  async function loadSiteConfig() {
242
329
  const configPath = resolve(cwd, foundConfig)
243
330
  const raw = fs.readFileSync(configPath, 'utf-8')
244
331
 
245
- const isTs = /\.m?ts$/.test(foundConfig)
246
- const { code } = await transform(raw, {
247
- loader: isTs ? 'ts' : 'js',
248
- format: 'esm',
249
- })
250
-
251
- const stubbed = code.replace(
252
- /import\s*\{[^}]*defineConfig[^}]*\}\s*from\s*['"][^'"]*['"]\s*;?/g,
253
- 'const defineConfig = (c) => c;',
332
+ // Stub the framework's value imports so the config evaluates without the real (browser-only)
333
+ // package. `defineConfig` is identity (the config object passes through); every other named
334
+ // import (e.g. `tk`, `localizedPage`) becomes a callable no-op that returns a no-op, covering
335
+ // helpers used as values. Relative imports (e.g. `./locales`) are left intact and bundled below.
336
+ let stubbed = raw.replace(
337
+ /import\s*\{([^}]*)\}\s*from\s*['"][^'"]*vue-site['"]\s*;?/g,
338
+ (_match, names) => {
339
+ const ids = names
340
+ .split(',')
341
+ .map((part) => part.trim())
342
+ .filter(Boolean)
343
+ .map((part) => {
344
+ const segments = part.split(/\s+as\s+/)
345
+ return (segments[1] ?? segments[0]).trim()
346
+ })
347
+ .filter(Boolean)
348
+ return ids
349
+ .map((id) =>
350
+ id === 'defineConfig'
351
+ ? 'const defineConfig = (c) => c;'
352
+ : `const ${id} = (..._args) => (() => {});`,
353
+ )
354
+ .join('\n')
355
+ },
254
356
  )
255
357
 
256
- const tmpFile = resolve(cwd, `.site-config.${Date.now()}.tmp.mjs`)
257
- fs.writeFileSync(tmpFile, stubbed)
358
+ // `import.meta.glob(...)` is a Vite-only feature; in Node it would throw at config-eval time.
359
+ // Stub it to an empty map so configs using `localizedPageGlob(import.meta.glob(...))` pre-load
360
+ // (page loaders are never invoked here — only build-time settings are read). The real glob runs
361
+ // in the browser via Vite.
362
+ if (/import\.meta\.glob/.test(stubbed)) {
363
+ stubbed =
364
+ 'const __vueSiteGlobStub = (..._args) => ({});\n' +
365
+ stubbed.replace(/import\.meta\.glob/g, '__vueSiteGlobStub')
366
+ }
367
+
368
+ const isTs = /\.m?ts$/.test(foundConfig)
369
+ // Write the stubbed entry next to the original so its relative imports (`./locales`) resolve.
370
+ const entryFile = resolve(
371
+ dirname(configPath),
372
+ `.${basename(configPath)}.${Date.now()}.preload.${isTs ? 'ts' : 'js'}`,
373
+ )
374
+ const tmpDir = resolve(cwd, `.vue-site-preload-${Date.now()}`)
375
+ fs.writeFileSync(entryFile, stubbed)
258
376
 
259
377
  try {
260
- const mod = await import(pathToFileURL(tmpFile).href)
378
+ // Bundle so local modules the config imports (e.g. `./locales.ts`) are inlined and TS is
379
+ // handled; asset imports are stubbed; remaining bare deps stay external for Node to resolve.
380
+ await esbuild({
381
+ entryPoints: { 'site-config': entryFile },
382
+ outdir: tmpDir,
383
+ bundle: true,
384
+ format: 'esm',
385
+ platform: 'node',
386
+ splitting: true,
387
+ logLevel: 'silent',
388
+ packages: 'external',
389
+ outExtension: { '.js': '.mjs' },
390
+ plugins: [preloadAssetStubPlugin()],
391
+ })
392
+
393
+ const entry = resolve(tmpDir, 'site-config.mjs')
394
+ const mod = await import(pathToFileURL(entry).href)
261
395
  return mod.default || {}
262
396
  } catch (e) {
263
397
  throw new Error(
264
398
  `[vue-site] Could not pre-load site config from ${foundConfig}: ${e.message}\n` +
265
399
  ` This usually means your config imports modules Node can't resolve directly ` +
266
- `(path aliases like @/..., .vue/.css/asset imports, or framework APIs other than defineConfig).`,
400
+ `(path aliases like @/..., or framework APIs other than defineConfig).`,
267
401
  )
268
402
  } finally {
269
403
  try {
270
- fs.unlinkSync(tmpFile)
404
+ fs.unlinkSync(entryFile)
405
+ } catch {}
406
+ try {
407
+ fs.rmSync(tmpDir, { recursive: true, force: true })
271
408
  } catch {}
272
409
  }
273
410
  }
274
411
 
412
+ /**
413
+ * Turn a base file path into a `import.meta.glob(...)` expression matching the base file plus its
414
+ * per-locale siblings (`name.<code>.ext`) — but NOT unrelated `nameOther.ext`:
415
+ * `../README.md` -> import.meta.glob(["../README.md","../README.*.md"], { query: '?raw' })
416
+ * `./pages/Home.vue` -> import.meta.glob(["./pages/Home.vue","./pages/Home.*.vue"], {})
417
+ * Markdown is loaded `?raw` (string content); other files (e.g. `.vue`) export a component.
418
+ * Returns `null` when the path has no extension (can't build a sensible glob).
419
+ */
420
+ function fileToLocaleGlobExpr(rawPath) {
421
+ const qIdx = rawPath.indexOf('?')
422
+ const query = qIdx >= 0 ? rawPath.slice(qIdx + 1) : ''
423
+ const pathOnly = qIdx >= 0 ? rawPath.slice(0, qIdx) : rawPath
424
+ const dot = pathOnly.lastIndexOf('.')
425
+ if (dot <= pathOnly.lastIndexOf('/')) return null
426
+ const ext = pathOnly.slice(dot)
427
+ const globs = [pathOnly, `${pathOnly.slice(0, dot)}.*${ext}`]
428
+ const isMarkdown = /\.(?:md|markdown)$/i.test(ext) || /(?:^|&)raw(?:$|&)/.test(query)
429
+ const opts = isMarkdown ? `{ query: '?raw' }` : `{}`
430
+ return `import.meta.glob(${JSON.stringify(globs)}, ${opts})`
431
+ }
432
+
433
+ // Sugar so configs can name page files as plain strings; the CLI turns them into Vite globs so the
434
+ // per-locale files get bundled. Two shapes are rewritten in the user's JS/TS under `cwd`:
435
+ // localizedPage('./file.md') -> localizedPage(import.meta.glob([...], { query: '?raw' }))
436
+ // page: './file.md' -> page: import.meta.glob([...], { query: '?raw' })
437
+ // Loader functions, locale maps (`localizedPage({ en, zh })`) and explicit `import.meta.glob` are
438
+ // left untouched; the `page:` form only rewrites path-like values (starting with `.` or `/`).
439
+ function localizedPageSugarPlugin() {
440
+ const CALL_STRING_ARG = /(\blocalizedPage\s*\(\s*)(['"`])((?:\\.|(?!\2).)*)\2/g
441
+ const PAGE_STRING_FIELD = /(\bpage\s*:\s*)(['"`])((?:\\.|(?!\2).)*)\2/g
442
+ return {
443
+ name: 'vue-site:localized-page-sugar',
444
+ enforce: 'pre',
445
+ transform(code, id) {
446
+ const file = id.split('?')[0]
447
+ if (!/\.(?:[cm]?[jt]sx?)$/.test(file)) return
448
+ if (!file.startsWith(cwd) || file.includes('/node_modules/')) return
449
+ if (!code.includes('localizedPage(') && !/\bpage\s*:\s*['"`]/.test(code)) return
450
+ let changed = false
451
+ let out = code.replace(CALL_STRING_ARG, (match, head, _q, rawPath) => {
452
+ const expr = fileToLocaleGlobExpr(rawPath)
453
+ if (!expr) return match
454
+ changed = true
455
+ return `${head}${expr}`
456
+ })
457
+ out = out.replace(PAGE_STRING_FIELD, (match, head, _q, rawPath) => {
458
+ // Only rewrite path-like values to avoid touching unrelated `page: '...'` properties.
459
+ if (!/^[./]/.test(rawPath)) return match
460
+ const expr = fileToLocaleGlobExpr(rawPath)
461
+ if (!expr) return match
462
+ changed = true
463
+ return `${head}${expr}`
464
+ })
465
+ return changed ? { code: out, map: null } : undefined
466
+ },
467
+ }
468
+ }
469
+
275
470
  function vueSitePlugin(entryCode) {
276
471
  return [
277
472
  {
@@ -459,12 +654,23 @@ async function buildViteConfig(options = {}) {
459
654
 
460
655
  const baseConfig = {
461
656
  root: cwd,
462
- plugins: [vue(vueOpts), ...watchedScssPlugin, ...vueSitePlugin(entryCode), ...(userPlugins || [])],
657
+ plugins: [localizedPageSugarPlugin(), vue(vueOpts), ...watchedScssPlugin, ...vueSitePlugin(entryCode), ...(userPlugins || [])],
463
658
  resolve: {
464
- alias: {
465
- vue: resolve(vuePath, 'dist/vue.runtime.esm-bundler.js'),
466
- 'vue-router': resolve(vueRouterPath, 'dist/vue-router.mjs'),
467
- },
659
+ alias: [
660
+ { find: /^@bndynet\/vue-site$/, replacement: frameworkEntry },
661
+ {
662
+ find: /^@bndynet\/vue-site\/style\.css$/,
663
+ replacement: frameworkStyle,
664
+ },
665
+ {
666
+ find: 'vue',
667
+ replacement: resolve(vuePath, 'dist/vue.runtime.esm-bundler.js'),
668
+ },
669
+ {
670
+ find: 'vue-router',
671
+ replacement: resolve(vueRouterPath, 'dist/vue-router.mjs'),
672
+ },
673
+ ],
468
674
  },
469
675
  optimizeDeps: {
470
676
  // Pre-bundle element-plus so Vite crawls its dependency graph and
@@ -474,6 +680,10 @@ async function buildViteConfig(options = {}) {
474
680
  // The virtual entry only imports element-plus CSS, so Vite's static
475
681
  // analysis never sees the JS side — this include bridges that gap.
476
682
  include: ['element-plus'],
683
+ // The virtual entry and user-authored pages both import the framework.
684
+ // Keep it out of dependency pre-bundling so Vite does not instantiate
685
+ // `dist/index.es.js` once via /@fs and again via node_modules/.vite.
686
+ exclude: [FRAMEWORK_PACKAGE],
477
687
  },
478
688
  server: {
479
689
  open: true,
@@ -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
+ };
@@ -4,6 +4,7 @@ export interface SiteContext {
4
4
  config: SiteConfig;
5
5
  resolvedNav: ResolvedNavItem[];
6
6
  }
7
+ /** Use `Symbol.for` so pages still inject context if Vite resolves the package twice. */
7
8
  export declare const siteContextKey: InjectionKey<SiteContext>;
8
9
  export declare function provideSiteConfig(context: SiteContext): void;
9
10
  export declare function useSiteConfig(): SiteContext;
@@ -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;