@moku-labs/web 1.14.0 → 1.15.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.
@@ -230,6 +230,28 @@ type Api$3 = {
230
230
  t(locale: string, key: string): string;
231
231
  };
232
232
  //#endregion
233
+ //#region src/plugins/i18n/index.d.ts
234
+ /**
235
+ * Internationalization plugin — locale registry plus a flat translation helper
236
+ * with default-locale fallback. Pure config-as-data (no state or events);
237
+ * consumed read-only by content, router, head, and build.
238
+ *
239
+ * @example Register locales and translations
240
+ * ```ts
241
+ * const app = createApp({
242
+ * pluginConfigs: {
243
+ * i18n: {
244
+ * locales: ["en", "uk"],
245
+ * defaultLocale: "en",
246
+ * localeNames: { en: "English", uk: "Українська" },
247
+ * translations: { uk: { "nav.home": "Головна" } }
248
+ * }
249
+ * }
250
+ * });
251
+ * ```
252
+ */
253
+ declare const i18nPlugin: import("@moku-labs/core").PluginInstance<"i18n", Config$3, Record<string, never>, Api$3, {}> & Record<never, never>;
254
+ //#endregion
233
255
  //#region src/plugins/router/iso-match.d.ts
234
256
  /**
235
257
  * A compiled, engine-agnostic path matcher: the same `.exec({ pathname })` shape the
@@ -1191,28 +1213,6 @@ type SpaApi = {
1191
1213
  */
1192
1214
  declare const sitePlugin: import("@moku-labs/core").PluginInstance<"site", Config$4, Record<string, never>, Api$4, {}> & Record<never, never>;
1193
1215
  //#endregion
1194
- //#region src/plugins/i18n/index.d.ts
1195
- /**
1196
- * Internationalization plugin — locale registry plus a flat translation helper
1197
- * with default-locale fallback. Pure config-as-data (no state or events);
1198
- * consumed read-only by content, router, head, and build.
1199
- *
1200
- * @example Register locales and translations
1201
- * ```ts
1202
- * const app = createApp({
1203
- * pluginConfigs: {
1204
- * i18n: {
1205
- * locales: ["en", "uk"],
1206
- * defaultLocale: "en",
1207
- * localeNames: { en: "English", uk: "Українська" },
1208
- * translations: { uk: { "nav.home": "Головна" } }
1209
- * }
1210
- * }
1211
- * });
1212
- * ```
1213
- */
1214
- declare const i18nPlugin: import("@moku-labs/core").PluginInstance<"i18n", Config$3, Record<string, never>, Api$3, {}> & Record<never, never>;
1215
- //#endregion
1216
1216
  //#region src/plugins/router/builders/route-builder.d.ts
1217
1217
  /**
1218
1218
  * Create a fluent route builder from a URL pattern string. Captures the pattern
@@ -1276,8 +1276,8 @@ declare function createUrls<T extends RouteMap>(routes: T, defaultLocale?: strin
1276
1276
  /**
1277
1277
  * Router plugin — typed, named route definitions with locale-aware URL generation
1278
1278
  * and matching. Author routes with {@link route}, then register them the normal config
1279
- * way via `pluginConfigs.router.routes` (compiled at init). Depends on site (base URL)
1280
- * and i18n (locales).
1279
+ * way via `pluginConfigs.router.routes` (compiled at init). Depends on site (base URL);
1280
+ * i18n (locales) is OPTIONAL — falls back to a single default locale ("en") when absent.
1281
1281
  *
1282
1282
  * @example Register routes via config, then start/build
1283
1283
  * ```ts
@@ -1380,7 +1380,7 @@ declare function buildArticleHead(articleMeta: ArticleMeta, canonicalUrl: string
1380
1380
  * Head plugin — composes per-route `<head>` metadata (title template, Open Graph,
1381
1381
  * Twitter cards, canonical, hreflang). Use the re-exported SEO primitives
1382
1382
  * ({@link meta}, {@link og}, {@link twitter}, …) inside a route's `.head()`.
1383
- * Depends on site, i18n, and router.
1383
+ * Depends on site and router; i18n is OPTIONAL (single default-locale fallback when absent).
1384
1384
  *
1385
1385
  * @example Set global head defaults
1386
1386
  * ```ts
@@ -2090,7 +2090,8 @@ type Api = {
2090
2090
  * (locale fallback, draft filtering, sort, caching, events) lives here; source I/O +
2091
2091
  * the Markdown pipeline live in a {@link ContentProvider} you compose (like `env`
2092
2092
  * providers). The shell imports zero node code, so `contentPlugin` is browser-safe.
2093
- * Depends on i18n; emits `content:ready` and `content:invalidated`.
2093
+ * i18n is OPTIONAL (single default-locale fallback when absent); emits `content:ready`
2094
+ * and `content:invalidated`.
2094
2095
  *
2095
2096
  * @example Compose the node filesystem provider with a content dir + Shiki theme
2096
2097
  * ```ts
package/dist/browser.mjs CHANGED
@@ -49,166 +49,9 @@ const createPlugin$1 = coreConfig.createPlugin;
49
49
  */
50
50
  const createCore = coreConfig.createCore;
51
51
  //#endregion
52
- //#region src/plugins/i18n/api.ts
53
- /** Error prefix for all i18n lifecycle failures. */
54
- const ERROR_PREFIX$8 = "[web]";
55
- /**
56
- * Validates the resolved i18n config (fail-fast at `createApp`). Throws when
57
- * `locales` is empty or when `defaultLocale` is not a member of `locales`.
58
- * Errors use the `[web]` prefix with an actionable remediation line.
59
- *
60
- * @param ctx - Plugin context carrying the resolved {@link Config}.
61
- * @param ctx.config - The resolved i18n {@link Config}.
62
- * @throws {Error} If `locales` is empty or `defaultLocale` is not in `locales`.
63
- * @example
64
- * ```ts
65
- * validateI18nConfig({ config: { locales: ["en"], defaultLocale: "en" } });
66
- * ```
67
- */
68
- function validateI18nConfig(ctx) {
69
- const { locales, defaultLocale } = ctx.config;
70
- if (locales.length === 0) throw new Error(`${ERROR_PREFIX$8} i18n.locales must contain at least one locale.\n Set pluginConfigs.i18n.locales to a non-empty array, e.g. ["en"].`);
71
- if (!locales.includes(defaultLocale)) throw new Error(`${ERROR_PREFIX$8} i18n.defaultLocale "${defaultLocale}" is not in i18n.locales [${locales.join(", ")}].\n Set pluginConfigs.i18n.defaultLocale to one of the configured locales, or add "${defaultLocale}" to i18n.locales.`);
72
- }
73
- /**
74
- * Creates the i18n plugin API surface — locale registry accessors plus the
75
- * `t()` translator with default-locale fallback. Every method is a pure read
76
- * of `ctx.config`; none mutate, and `t()` always returns a string.
77
- *
78
- * @param ctx - Plugin context carrying the resolved {@link Config}.
79
- * @param ctx.config - The resolved i18n {@link Config}.
80
- * @returns The {@link Api} accessor surface mounted at `app.i18n`.
81
- * @example
82
- * ```ts
83
- * const api = createI18nApi({ config: { locales: ["en"], defaultLocale: "en" } });
84
- * api.t("en", "nav.home");
85
- * ```
86
- */
87
- function createI18nApi(ctx) {
88
- const { config } = ctx;
89
- return {
90
- /**
91
- * Returns the configured supported locales in declared order.
92
- *
93
- * @returns The configured `locales` list (priority/display order).
94
- * @example
95
- * ```ts
96
- * api.locales(); // ["en", "uk"]
97
- * ```
98
- */
99
- locales() {
100
- return config.locales;
101
- },
102
- /**
103
- * Returns the fallback locale used when a requested locale is absent.
104
- *
105
- * @returns The configured `defaultLocale`.
106
- * @example
107
- * ```ts
108
- * api.defaultLocale(); // "en"
109
- * ```
110
- */
111
- defaultLocale() {
112
- return config.defaultLocale;
113
- },
114
- /**
115
- * Membership guard: whether `x` is one of the supported locales
116
- * (case-sensitive).
117
- *
118
- * @param x - Candidate locale code.
119
- * @returns `true` if `x ∈ locales`, else `false`.
120
- * @example
121
- * ```ts
122
- * api.isLocale("uk"); // true
123
- * ```
124
- */
125
- isLocale(x) {
126
- return config.locales.includes(x);
127
- },
128
- /**
129
- * Human-readable display name for a locale.
130
- *
131
- * @param locale - Locale code to look up.
132
- * @returns The display name, or `undefined` if unmapped.
133
- * @example
134
- * ```ts
135
- * api.localeName("uk"); // "Українська"
136
- * ```
137
- */
138
- localeName(locale) {
139
- return config.localeNames?.[locale];
140
- },
141
- /**
142
- * Open Graph `og:locale` value for a locale.
143
- *
144
- * @param locale - Locale code to look up.
145
- * @returns The `og:locale` value (e.g. `"en_US"`), or `undefined` if unmapped.
146
- * @example
147
- * ```ts
148
- * api.ogLocale("en"); // "en_US"
149
- * ```
150
- */
151
- ogLocale(locale) {
152
- return config.ogLocaleMap?.[locale];
153
- },
154
- /**
155
- * Translate `key` for `locale` with a deterministic fallback chain
156
- * (requested locale → default locale → the key itself). The default-locale
157
- * lookup is skipped when `locale === defaultLocale`.
158
- *
159
- * @param locale - Requested locale code.
160
- * @param key - Translation key (e.g. `"nav.home"`).
161
- * @returns The translated value, the default-locale value, or `key`.
162
- * @example
163
- * ```ts
164
- * api.t("uk", "nav.home"); // "Головна"
165
- * ```
166
- */
167
- t(locale, key) {
168
- const exact = config.translations?.[locale]?.[key];
169
- if (exact !== void 0) return exact;
170
- if (locale !== config.defaultLocale) {
171
- const fallback = config.translations?.[config.defaultLocale]?.[key];
172
- if (fallback !== void 0) return fallback;
173
- }
174
- return key;
175
- }
176
- };
177
- }
178
- /**
179
- * Internationalization plugin — locale registry plus a flat translation helper
180
- * with default-locale fallback. Pure config-as-data (no state or events);
181
- * consumed read-only by content, router, head, and build.
182
- *
183
- * @example Register locales and translations
184
- * ```ts
185
- * const app = createApp({
186
- * pluginConfigs: {
187
- * i18n: {
188
- * locales: ["en", "uk"],
189
- * defaultLocale: "en",
190
- * localeNames: { en: "English", uk: "Українська" },
191
- * translations: { uk: { "nav.home": "Головна" } }
192
- * }
193
- * }
194
- * });
195
- * ```
196
- */
197
- const i18nPlugin = createPlugin$1("i18n", {
198
- config: {
199
- locales: ["en"],
200
- defaultLocale: "en",
201
- localeNames: {},
202
- ogLocaleMap: {},
203
- translations: {}
204
- },
205
- onInit: validateI18nConfig,
206
- api: createI18nApi
207
- });
208
- //#endregion
209
52
  //#region src/plugins/site/api.ts
210
53
  /** Error prefix for all site lifecycle/validation failures. */
211
- const ERROR_PREFIX$7 = "[web]";
54
+ const ERROR_PREFIX$8 = "[web]";
212
55
  /** URL protocols that qualify a parsed URL as an absolute http/https URL. */
213
56
  const HTTP_PROTOCOLS = new Set(["http:", "https:"]);
214
57
  /**
@@ -307,8 +150,8 @@ function isAbsoluteUrl(value) {
307
150
  * ```
308
151
  */
309
152
  function validateSiteConfig(ctx) {
310
- if (!isNonEmpty(ctx.config.name)) throw new Error(`${ERROR_PREFIX$7} site.name is required.\n Provide a non-empty site name in pluginConfigs.site.name.`);
311
- if (!isAbsoluteUrl(ctx.config.url)) throw new Error(`${ERROR_PREFIX$7} site.url must be a valid absolute URL (http/https), received ${JSON.stringify(ctx.config.url)}.\n Provide an absolute URL in pluginConfigs.site.url, e.g. "https://blog.dev".`);
153
+ if (!isNonEmpty(ctx.config.name)) throw new Error(`${ERROR_PREFIX$8} site.name is required.\n Provide a non-empty site name in pluginConfigs.site.name.`);
154
+ if (!isAbsoluteUrl(ctx.config.url)) throw new Error(`${ERROR_PREFIX$8} site.url must be a valid absolute URL (http/https), received ${JSON.stringify(ctx.config.url)}.\n Provide an absolute URL in pluginConfigs.site.url, e.g. "https://blog.dev".`);
312
155
  }
313
156
  /**
314
157
  * Creates the site plugin API surface — read-only accessors over frozen config
@@ -421,6 +264,202 @@ const sitePlugin = createPlugin$1("site", {
421
264
  api: createSiteApi
422
265
  });
423
266
  //#endregion
267
+ //#region src/plugins/i18n/api.ts
268
+ /** Error prefix for all i18n lifecycle failures. */
269
+ const ERROR_PREFIX$7 = "[web]";
270
+ /**
271
+ * The framework's default i18n config — a single `"en"` locale with empty lookup
272
+ * maps. Used both as the i18n plugin's `config` default and as the source for
273
+ * {@link fallbackI18n}, so "no i18n config" and "no i18n plugin" resolve identically.
274
+ *
275
+ * @example
276
+ * ```ts
277
+ * createI18nApi({ config: DEFAULT_I18N_CONFIG }).defaultLocale(); // "en"
278
+ * ```
279
+ */
280
+ const DEFAULT_I18N_CONFIG = {
281
+ locales: ["en"],
282
+ defaultLocale: "en",
283
+ localeNames: {},
284
+ ogLocaleMap: {},
285
+ translations: {}
286
+ };
287
+ /**
288
+ * Validates the resolved i18n config (fail-fast at `createApp`). Throws when
289
+ * `locales` is empty or when `defaultLocale` is not a member of `locales`.
290
+ * Errors use the `[web]` prefix with an actionable remediation line.
291
+ *
292
+ * @param ctx - Plugin context carrying the resolved {@link Config}.
293
+ * @param ctx.config - The resolved i18n {@link Config}.
294
+ * @throws {Error} If `locales` is empty or `defaultLocale` is not in `locales`.
295
+ * @example
296
+ * ```ts
297
+ * validateI18nConfig({ config: { locales: ["en"], defaultLocale: "en" } });
298
+ * ```
299
+ */
300
+ function validateI18nConfig(ctx) {
301
+ const { locales, defaultLocale } = ctx.config;
302
+ if (locales.length === 0) throw new Error(`${ERROR_PREFIX$7} i18n.locales must contain at least one locale.\n Set pluginConfigs.i18n.locales to a non-empty array, e.g. ["en"].`);
303
+ if (!locales.includes(defaultLocale)) throw new Error(`${ERROR_PREFIX$7} i18n.defaultLocale "${defaultLocale}" is not in i18n.locales [${locales.join(", ")}].\n Set pluginConfigs.i18n.defaultLocale to one of the configured locales, or add "${defaultLocale}" to i18n.locales.`);
304
+ }
305
+ /**
306
+ * Creates the i18n plugin API surface — locale registry accessors plus the
307
+ * `t()` translator with default-locale fallback. Every method is a pure read
308
+ * of `ctx.config`; none mutate, and `t()` always returns a string.
309
+ *
310
+ * @param ctx - Plugin context carrying the resolved {@link Config}.
311
+ * @param ctx.config - The resolved i18n {@link Config}.
312
+ * @returns The {@link Api} accessor surface mounted at `app.i18n`.
313
+ * @example
314
+ * ```ts
315
+ * const api = createI18nApi({ config: { locales: ["en"], defaultLocale: "en" } });
316
+ * api.t("en", "nav.home");
317
+ * ```
318
+ */
319
+ function createI18nApi(ctx) {
320
+ const { config } = ctx;
321
+ return {
322
+ /**
323
+ * Returns the configured supported locales in declared order.
324
+ *
325
+ * @returns The configured `locales` list (priority/display order).
326
+ * @example
327
+ * ```ts
328
+ * api.locales(); // ["en", "uk"]
329
+ * ```
330
+ */
331
+ locales() {
332
+ return config.locales;
333
+ },
334
+ /**
335
+ * Returns the fallback locale used when a requested locale is absent.
336
+ *
337
+ * @returns The configured `defaultLocale`.
338
+ * @example
339
+ * ```ts
340
+ * api.defaultLocale(); // "en"
341
+ * ```
342
+ */
343
+ defaultLocale() {
344
+ return config.defaultLocale;
345
+ },
346
+ /**
347
+ * Membership guard: whether `x` is one of the supported locales
348
+ * (case-sensitive).
349
+ *
350
+ * @param x - Candidate locale code.
351
+ * @returns `true` if `x ∈ locales`, else `false`.
352
+ * @example
353
+ * ```ts
354
+ * api.isLocale("uk"); // true
355
+ * ```
356
+ */
357
+ isLocale(x) {
358
+ return config.locales.includes(x);
359
+ },
360
+ /**
361
+ * Human-readable display name for a locale.
362
+ *
363
+ * @param locale - Locale code to look up.
364
+ * @returns The display name, or `undefined` if unmapped.
365
+ * @example
366
+ * ```ts
367
+ * api.localeName("uk"); // "Українська"
368
+ * ```
369
+ */
370
+ localeName(locale) {
371
+ return config.localeNames?.[locale];
372
+ },
373
+ /**
374
+ * Open Graph `og:locale` value for a locale.
375
+ *
376
+ * @param locale - Locale code to look up.
377
+ * @returns The `og:locale` value (e.g. `"en_US"`), or `undefined` if unmapped.
378
+ * @example
379
+ * ```ts
380
+ * api.ogLocale("en"); // "en_US"
381
+ * ```
382
+ */
383
+ ogLocale(locale) {
384
+ return config.ogLocaleMap?.[locale];
385
+ },
386
+ /**
387
+ * Translate `key` for `locale` with a deterministic fallback chain
388
+ * (requested locale → default locale → the key itself). The default-locale
389
+ * lookup is skipped when `locale === defaultLocale`.
390
+ *
391
+ * @param locale - Requested locale code.
392
+ * @param key - Translation key (e.g. `"nav.home"`).
393
+ * @returns The translated value, the default-locale value, or `key`.
394
+ * @example
395
+ * ```ts
396
+ * api.t("uk", "nav.home"); // "Головна"
397
+ * ```
398
+ */
399
+ t(locale, key) {
400
+ const exact = config.translations?.[locale]?.[key];
401
+ if (exact !== void 0) return exact;
402
+ if (locale !== config.defaultLocale) {
403
+ const fallback = config.translations?.[config.defaultLocale]?.[key];
404
+ if (fallback !== void 0) return fallback;
405
+ }
406
+ return key;
407
+ }
408
+ };
409
+ }
410
+ /**
411
+ * The i18n API a consumer sees when the i18n plugin is NOT composed: a single
412
+ * default locale (`"en"`) with empty maps. `locales()` is `["en"]`,
413
+ * `defaultLocale()` is `"en"`, and every map lookup misses (`undefined`, or the
414
+ * key for `t()`). Identical to composing the i18n plugin with its defaults — which
415
+ * is what makes i18n optional: `router`/`head`/`content`/`build` fall back to this
416
+ * when `ctx.has("i18n")` is false, leaving every downstream call unchanged.
417
+ *
418
+ * @example
419
+ * ```ts
420
+ * const i18n = ctx.has("i18n") ? ctx.require(i18nPlugin) : fallbackI18n;
421
+ * i18n.locales(); // ["en"]
422
+ * ```
423
+ */
424
+ const fallbackI18n = createI18nApi({ config: DEFAULT_I18N_CONFIG });
425
+ //#endregion
426
+ //#region src/plugins/i18n/index.ts
427
+ /**
428
+ * i18n — Micro tier. Multi-file layout (index wiring + api.ts + types.ts) so
429
+ * index.ts stays within the ≤30-line wiring-only hook; logic lives in api.ts.
430
+ *
431
+ * Locale registry + flat translation helper with default-locale fallback.
432
+ * Pure config-as-data: no state, no events, no lifecycle resources.
433
+ * Consumed read-only by content/router/head/build via `ctx.require(i18nPlugin)`.
434
+ *
435
+ * @file i18n plugin wiring harness.
436
+ * @see README.md
437
+ */
438
+ /**
439
+ * Internationalization plugin — locale registry plus a flat translation helper
440
+ * with default-locale fallback. Pure config-as-data (no state or events);
441
+ * consumed read-only by content, router, head, and build.
442
+ *
443
+ * @example Register locales and translations
444
+ * ```ts
445
+ * const app = createApp({
446
+ * pluginConfigs: {
447
+ * i18n: {
448
+ * locales: ["en", "uk"],
449
+ * defaultLocale: "en",
450
+ * localeNames: { en: "English", uk: "Українська" },
451
+ * translations: { uk: { "nav.home": "Головна" } }
452
+ * }
453
+ * }
454
+ * });
455
+ * ```
456
+ */
457
+ const i18nPlugin = createPlugin$1("i18n", {
458
+ config: DEFAULT_I18N_CONFIG,
459
+ onInit: validateI18nConfig,
460
+ api: createI18nApi
461
+ });
462
+ //#endregion
424
463
  //#region src/plugins/router/iso-match.ts
425
464
  /**
426
465
  * Parse a single path segment into its `{…}` placeholder, or `false` for a static
@@ -1004,8 +1043,9 @@ function compileRoutes(input) {
1004
1043
  const ERROR_PREFIX$5 = "[web] router";
1005
1044
  /**
1006
1045
  * Validate a route map and compile it into the matcher table on `ctx.state`,
1007
- * resolving the global render `mode` + site base URL + i18n locales at call time.
1008
- * Called by the router's `onInit` to compile `config.routes`. Re-calling replaces the table.
1046
+ * resolving the global render `mode` + site base URL + i18n locales (or the single
1047
+ * default-locale fallback when i18n is not composed) at call time. Called by the
1048
+ * router's `onInit` to compile `config.routes`. Re-calling replaces the table.
1009
1049
  *
1010
1050
  * @param ctx - The router register context (state + global mode + require).
1011
1051
  * @param routes - The route map to compile (an `import * as routes` namespace works).
@@ -1017,7 +1057,7 @@ const ERROR_PREFIX$5 = "[web] router";
1017
1057
  */
1018
1058
  function registerRoutes(ctx, routes) {
1019
1059
  validateRoutes(routes);
1020
- const i18n = ctx.require(i18nPlugin);
1060
+ const i18n = ctx.has("i18n") ? ctx.require(i18nPlugin) : fallbackI18n;
1021
1061
  ctx.state.table = compileRoutes({
1022
1062
  routes,
1023
1063
  mode: ctx.global.mode,
@@ -1442,8 +1482,8 @@ function createState$2(_ctx) {
1442
1482
  /**
1443
1483
  * Router plugin — typed, named route definitions with locale-aware URL generation
1444
1484
  * and matching. Author routes with {@link route}, then register them the normal config
1445
- * way via `pluginConfigs.router.routes` (compiled at init). Depends on site (base URL)
1446
- * and i18n (locales).
1485
+ * way via `pluginConfigs.router.routes` (compiled at init). Depends on site (base URL);
1486
+ * i18n (locales) is OPTIONAL — falls back to a single default locale ("en") when absent.
1447
1487
  *
1448
1488
  * @example Register routes via config, then start/build
1449
1489
  * ```ts
@@ -1456,7 +1496,7 @@ function createState$2(_ctx) {
1456
1496
  * ```
1457
1497
  */
1458
1498
  const routerPlugin = createPlugin$1("router", {
1459
- depends: [sitePlugin, i18nPlugin],
1499
+ depends: [sitePlugin],
1460
1500
  helpers: {
1461
1501
  route,
1462
1502
  defineRoutes,
@@ -1898,7 +1938,8 @@ function serializeHead(elements) {
1898
1938
  *
1899
1939
  * The `render` method pulls `site`/`i18n`/`router` via `ctx.require` at call time,
1900
1940
  * composes the head element set via the shared `compose.ts` module, and serializes
1901
- * it to a string. It holds no resource and caches no subscription.
1941
+ * it to a string. It holds no resource and caches no subscription. `i18n` is
1942
+ * OPTIONAL — a single default-locale fallback is used when it is not composed.
1902
1943
  */
1903
1944
  /** Error prefix for head API invariant failures. */
1904
1945
  const ERROR_PREFIX$4 = "[web] head";
@@ -1950,7 +1991,7 @@ function createApi$1(ctx) {
1950
1991
  data,
1951
1992
  defaults: readDefaults(ctx.state),
1952
1993
  site: ctx.require(sitePlugin),
1953
- i18n: ctx.require(i18nPlugin),
1994
+ i18n: ctx.has("i18n") ? ctx.require(i18nPlugin) : fallbackI18n,
1954
1995
  router: ctx.require(routerPlugin)
1955
1996
  }));
1956
1997
  },
@@ -1970,7 +2011,8 @@ function createApi$1(ctx) {
1970
2011
  */
1971
2012
  siteHead(input) {
1972
2013
  const site = ctx.require(sitePlugin);
1973
- const ogLocale = input.locale === void 0 ? void 0 : ctx.require(i18nPlugin).ogLocale(input.locale);
2014
+ const i18n = ctx.has("i18n") ? ctx.require(i18nPlugin) : fallbackI18n;
2015
+ const ogLocale = input.locale === void 0 ? void 0 : i18n.ogLocale(input.locale);
1974
2016
  return serializeHead(composeSiteHead({
1975
2017
  site,
1976
2018
  defaults: readDefaults(ctx.state),
@@ -2107,7 +2149,7 @@ function createState$1(_ctx) {
2107
2149
  * Head plugin — composes per-route `<head>` metadata (title template, Open Graph,
2108
2150
  * Twitter cards, canonical, hreflang). Use the re-exported SEO primitives
2109
2151
  * ({@link meta}, {@link og}, {@link twitter}, …) inside a route's `.head()`.
2110
- * Depends on site, i18n, and router.
2152
+ * Depends on site and router; i18n is OPTIONAL (single default-locale fallback when absent).
2111
2153
  *
2112
2154
  * @example Set global head defaults
2113
2155
  * ```ts
@@ -2123,11 +2165,7 @@ function createState$1(_ctx) {
2123
2165
  * ```
2124
2166
  */
2125
2167
  const headPlugin = createPlugin$1("head", {
2126
- depends: [
2127
- sitePlugin,
2128
- i18nPlugin,
2129
- routerPlugin
2130
- ],
2168
+ depends: [sitePlugin, routerPlugin],
2131
2169
  helpers: headHelpers,
2132
2170
  config: defaultConfig,
2133
2171
  createState: createState$1,
@@ -3959,7 +3997,8 @@ function articleNotFound(slug, locale) {
3959
3997
  return /* @__PURE__ */ new Error(`[web] content article "${slug}" not found for locale "${locale}".\n Looked for ${slug}/${locale}.md and the default-locale fallback.`);
3960
3998
  }
3961
3999
  /**
3962
- * Plugin `api` factory: resolves i18n via `ctx.require`, merges `config.providers` into
4000
+ * Plugin `api` factory: resolves i18n via `ctx.require` (or the single default-locale
4001
+ * fallback when i18n is not composed), merges `config.providers` into
3963
4002
  * one source, assembles the kernel-free {@link ContentApiContext}, and delegates to
3964
4003
  * {@link createContentApi}. Referenced directly as the plugin's `api` so index.ts stays
3965
4004
  * wiring-only. Imports no node code (the provider owns it).
@@ -3972,7 +4011,7 @@ function articleNotFound(slug, locale) {
3972
4011
  * ```
3973
4012
  */
3974
4013
  function contentApi(ctx) {
3975
- const i18nApi = ctx.require(i18nPlugin);
4014
+ const i18nApi = ctx.has("i18n") ? ctx.require(i18nPlugin) : fallbackI18n;
3976
4015
  /**
3977
4016
  * Active locale codes from i18n.
3978
4017
  *
@@ -4377,8 +4416,8 @@ function validateContentConfig(config) {
4377
4416
  * @file content — Complex Plugin skeleton (wiring-only).
4378
4417
  *
4379
4418
  * Markdown pipeline: discover, parse frontmatter, render to sanitized HTML, and
4380
- * expose a locale-keyed Article model. Depends on i18n. Emits `content:ready`
4381
- * and `content:invalidated`.
4419
+ * expose a locale-keyed Article model. i18n is OPTIONAL (single default-locale
4420
+ * fallback when absent). Emits `content:ready` and `content:invalidated`.
4382
4421
  * @see README.md
4383
4422
  */
4384
4423
  /**
@@ -4386,7 +4425,8 @@ function validateContentConfig(config) {
4386
4425
  * (locale fallback, draft filtering, sort, caching, events) lives here; source I/O +
4387
4426
  * the Markdown pipeline live in a {@link ContentProvider} you compose (like `env`
4388
4427
  * providers). The shell imports zero node code, so `contentPlugin` is browser-safe.
4389
- * Depends on i18n; emits `content:ready` and `content:invalidated`.
4428
+ * i18n is OPTIONAL (single default-locale fallback when absent); emits `content:ready`
4429
+ * and `content:invalidated`.
4390
4430
  *
4391
4431
  * @example Compose the node filesystem provider with a content dir + Shiki theme
4392
4432
  * ```ts
@@ -4402,7 +4442,6 @@ function validateContentConfig(config) {
4402
4442
  * ```
4403
4443
  */
4404
4444
  const contentPlugin = createPlugin$1("content", {
4405
- depends: [i18nPlugin],
4406
4445
  events: contentEvents,
4407
4446
  config: defaultContentConfig,
4408
4447
  createState: createContentState,