@finesoft/front 0.1.52 → 0.1.54

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/dist/index.mjs CHANGED
@@ -85,9 +85,10 @@ var IntentDispatcher = class {
85
85
  };
86
86
  //#endregion
87
87
  //#region ../core/src/dependencies/container.ts
88
- var Container = class {
88
+ var Container = class Container {
89
89
  registrations = /* @__PURE__ */ new Map();
90
90
  resolutionStack = /* @__PURE__ */ new Set();
91
+ parent;
91
92
  /** 注册依赖(默认单例) */
92
93
  register(key, factory, singleton = true) {
93
94
  this.registrations.set(key, {
@@ -96,10 +97,13 @@ var Container = class {
96
97
  });
97
98
  return this;
98
99
  }
99
- /** 解析依赖 */
100
+ /** 解析依赖 — 当前容器未注册时回退到 parent */
100
101
  resolve(key) {
101
102
  const reg = this.registrations.get(key);
102
- if (!reg) throw new Error(`[Container] No registration for key: "${key}"`);
103
+ if (!reg) {
104
+ if (this.parent) return this.parent.resolve(key);
105
+ throw new Error(`[Container] No registration for key: "${key}"`);
106
+ }
103
107
  if (reg.singleton) {
104
108
  if (reg.instance === void 0) {
105
109
  if (this.resolutionStack.has(key)) throw new Error(`[Container] Circular dependency detected: ${[...this.resolutionStack, key].join(" → ")}`);
@@ -114,9 +118,20 @@ var Container = class {
114
118
  }
115
119
  return reg.factory();
116
120
  }
117
- /** 检查是否已注册 */
121
+ /** 检查是否已注册(含 parent) */
118
122
  has(key) {
119
- return this.registrations.has(key);
123
+ return this.registrations.has(key) || (this.parent?.has(key) ?? false);
124
+ }
125
+ /**
126
+ * 创建子容器(请求级 scope)
127
+ *
128
+ * 子容器可覆写父容器的依赖(如每请求的 locale、user),
129
+ * 未覆写的 key 自动回退到父容器解析。
130
+ */
131
+ createScope() {
132
+ const child = new Container();
133
+ child.parent = this;
134
+ return child;
120
135
  }
121
136
  /** 销毁容器,清除所有缓存 */
122
137
  dispose() {
@@ -125,6 +140,101 @@ var Container = class {
125
140
  }
126
141
  };
127
142
  //#endregion
143
+ //#region ../core/src/i18n/locale.ts
144
+ /** RTL 语言列表 */
145
+ const RTL_LANGUAGES = new Set([
146
+ "ar",
147
+ "arc",
148
+ "dv",
149
+ "fa",
150
+ "ha",
151
+ "he",
152
+ "khw",
153
+ "ks",
154
+ "ku",
155
+ "ps",
156
+ "ur",
157
+ "yi"
158
+ ]);
159
+ /** 检测语言是否为 RTL */
160
+ function isRtl(language) {
161
+ const primary = language.split("-")[0].toLowerCase();
162
+ return RTL_LANGUAGES.has(primary);
163
+ }
164
+ /** 获取文本方向 */
165
+ function getTextDirection(language) {
166
+ return isRtl(language) ? "rtl" : "ltr";
167
+ }
168
+ /**
169
+ * 从语言代码生成 HTML lang/dir 属性
170
+ *
171
+ * @example
172
+ * ```ts
173
+ * getLocaleAttributes("ar-SA") // { lang: "ar-SA", dir: "rtl" }
174
+ * getLocaleAttributes("en-US") // { lang: "en-US", dir: "ltr" }
175
+ * ```
176
+ */
177
+ function getLocaleAttributes(language) {
178
+ return {
179
+ lang: language,
180
+ dir: getTextDirection(language)
181
+ };
182
+ }
183
+ /**
184
+ * 构建 LocaleInfo
185
+ *
186
+ * @param language - 语言代码(如 "zh-Hans")
187
+ * @param region - 地区代码(如 "CN"),可选
188
+ */
189
+ function makeLocaleInfo(language, region) {
190
+ return {
191
+ language,
192
+ region,
193
+ bcp47: region ? `${language}-${region}` : language,
194
+ dir: getTextDirection(language)
195
+ };
196
+ }
197
+ /**
198
+ * 将 locale 属性应用到 `<html>` 元素
199
+ *
200
+ * 服务端渲染时可用于字符串拼接,浏览器端直接操作 DOM。
201
+ */
202
+ function setHtmlLocaleAttributes(attrs) {
203
+ document.documentElement.lang = attrs.lang;
204
+ document.documentElement.dir = attrs.dir;
205
+ }
206
+ //#endregion
207
+ //#region ../core/src/logger/composite.ts
208
+ var CompositeLoggerFactory = class {
209
+ constructor(factories) {
210
+ this.factories = factories;
211
+ }
212
+ loggerFor(name) {
213
+ return new CompositeLogger(this.factories.map((f) => f.loggerFor(name)));
214
+ }
215
+ };
216
+ var CompositeLogger = class {
217
+ constructor(loggers) {
218
+ this.loggers = loggers;
219
+ }
220
+ debug(...args) {
221
+ return this.callAll("debug", args);
222
+ }
223
+ info(...args) {
224
+ return this.callAll("info", args);
225
+ }
226
+ warn(...args) {
227
+ return this.callAll("warn", args);
228
+ }
229
+ error(...args) {
230
+ return this.callAll("error", args);
231
+ }
232
+ callAll(method, args) {
233
+ for (const logger of this.loggers) logger[method](...args);
234
+ return "";
235
+ }
236
+ };
237
+ //#endregion
128
238
  //#region ../core/src/logger/base.ts
129
239
  var BaseLogger = class {
130
240
  category;
@@ -213,6 +323,111 @@ var ConsoleLoggerFactory = class {
213
323
  }
214
324
  };
215
325
  //#endregion
326
+ //#region ../core/src/logger/reporting.ts
327
+ /**
328
+ * ReportingLogger — 上报型日志实现
329
+ *
330
+ * 将 warn/error 级别日志转发到外部监控服务(如 Sentry、Datadog)。
331
+ * 用户通过 ReportCallback 注入上报逻辑,框架不直接依赖任何第三方 SDK。
332
+ */
333
+ const LEVEL_PRIORITY = {
334
+ debug: 0,
335
+ info: 1,
336
+ warn: 2,
337
+ error: 3
338
+ };
339
+ var ReportingLogger = class extends BaseLogger {
340
+ minPriority;
341
+ report;
342
+ constructor(category, options) {
343
+ super(category);
344
+ this.minPriority = LEVEL_PRIORITY[options.minLevel ?? "warn"];
345
+ this.report = options.report;
346
+ }
347
+ debug(...args) {
348
+ this.maybeReport("debug", args);
349
+ return "";
350
+ }
351
+ info(...args) {
352
+ this.maybeReport("info", args);
353
+ return "";
354
+ }
355
+ warn(...args) {
356
+ this.maybeReport("warn", args);
357
+ return "";
358
+ }
359
+ error(...args) {
360
+ this.maybeReport("error", args);
361
+ return "";
362
+ }
363
+ maybeReport(level, args) {
364
+ if (LEVEL_PRIORITY[level] >= this.minPriority) this.report(level, this.category, args);
365
+ }
366
+ };
367
+ var ReportingLoggerFactory = class {
368
+ options;
369
+ constructor(options) {
370
+ this.options = options;
371
+ }
372
+ loggerFor(category) {
373
+ return new ReportingLogger(category, this.options);
374
+ }
375
+ };
376
+ //#endregion
377
+ //#region ../core/src/metrics/console-recorder.ts
378
+ var ConsoleEventRecorder = class {
379
+ prefix;
380
+ constructor(prefix = "Metrics") {
381
+ this.prefix = prefix;
382
+ }
383
+ record(type, fields) {
384
+ console.info(`[${this.prefix}:${type}]`, fields ?? "");
385
+ }
386
+ async flush() {}
387
+ destroy() {}
388
+ };
389
+ //#endregion
390
+ //#region ../core/src/utils/platform.ts
391
+ /**
392
+ * 从 User-Agent 字符串解析平台信息
393
+ *
394
+ * @param ua - User-Agent 字符串(默认取 navigator.userAgent)
395
+ */
396
+ function detectPlatform(ua) {
397
+ const agent = ua ?? (typeof navigator !== "undefined" ? navigator.userAgent : "");
398
+ const lower = agent.toLowerCase();
399
+ return {
400
+ os: detectOS(lower),
401
+ browser: detectBrowser(lower),
402
+ engine: detectEngine(lower),
403
+ isMobile: /mobile|android|iphone|ipad|ipod/i.test(agent),
404
+ isTouch: typeof navigator !== "undefined" && "maxTouchPoints" in navigator ? navigator.maxTouchPoints > 0 : false
405
+ };
406
+ }
407
+ function detectOS(ua) {
408
+ if (/iphone|ipad|ipod/.test(ua)) return "ios";
409
+ if (/android/.test(ua)) return "android";
410
+ if (/macintosh|mac os x/.test(ua)) return "macos";
411
+ if (/windows/.test(ua)) return "windows";
412
+ if (/linux/.test(ua)) return "linux";
413
+ return "unknown";
414
+ }
415
+ function detectBrowser(ua) {
416
+ if (/edg\//.test(ua)) return "edge";
417
+ if (/opr\/|opera/.test(ua)) return "opera";
418
+ if (/samsungbrowser/.test(ua)) return "samsung";
419
+ if (/chrome|crios/.test(ua) && !/edg\//.test(ua)) return "chrome";
420
+ if (/firefox|fxios/.test(ua)) return "firefox";
421
+ if (/safari/.test(ua) && !/chrome/.test(ua)) return "safari";
422
+ return "unknown";
423
+ }
424
+ function detectEngine(ua) {
425
+ if (/applewebkit/.test(ua) && !/chrome/.test(ua)) return "webkit";
426
+ if (/applewebkit/.test(ua) && /chrome/.test(ua)) return "blink";
427
+ if (/gecko\//.test(ua)) return "gecko";
428
+ return "unknown";
429
+ }
430
+ //#endregion
216
431
  //#region ../core/src/dependencies/make-dependencies.ts
217
432
  /**
218
433
  * 依赖工厂 — 创建所有基础依赖
@@ -224,7 +439,10 @@ const DEP_KEYS = {
224
439
  STORAGE: "storage",
225
440
  FEATURE_FLAGS: "featureFlags",
226
441
  METRICS: "metrics",
227
- FETCH: "fetch"
442
+ FETCH: "fetch",
443
+ EVENT_RECORDER: "eventRecorder",
444
+ LOCALE: "locale",
445
+ PLATFORM: "platform"
228
446
  };
229
447
  var MemoryStorage = class {
230
448
  store = /* @__PURE__ */ new Map();
@@ -240,38 +458,67 @@ var MemoryStorage = class {
240
458
  };
241
459
  var DefaultFeatureFlags = class {
242
460
  flags;
461
+ providers = [];
243
462
  constructor(flags = {}) {
244
463
  this.flags = flags;
245
464
  }
465
+ /** 注册外部 provider(如远程配置、A/B 测试 SDK) */
466
+ addProvider(provider) {
467
+ this.providers.push(provider);
468
+ }
246
469
  isEnabled(key) {
470
+ for (let i = this.providers.length - 1; i >= 0; i--) if (this.providers[i].isEnabled(key)) return true;
247
471
  return this.flags[key] === true;
248
472
  }
249
473
  getString(key) {
474
+ for (let i = this.providers.length - 1; i >= 0; i--) {
475
+ const result = this.providers[i].getString?.(key);
476
+ if (result !== void 0) return result;
477
+ }
250
478
  const v = this.flags[key];
251
479
  return typeof v === "string" ? v : void 0;
252
480
  }
253
481
  getNumber(key) {
482
+ for (let i = this.providers.length - 1; i >= 0; i--) {
483
+ const result = this.providers[i].getNumber?.(key);
484
+ if (result !== void 0) return result;
485
+ }
254
486
  const v = this.flags[key];
255
487
  return typeof v === "number" ? v : void 0;
256
488
  }
257
489
  };
258
490
  var ConsoleMetrics = class {
491
+ record(type, fields) {
492
+ console.info(`[Metrics:${type}]`, fields ?? "");
493
+ }
259
494
  recordPageView(page, fields) {
260
- console.info(`[Metrics:PageView] ${page}`, fields ?? "");
495
+ this.record("PageView", {
496
+ page,
497
+ ...fields
498
+ });
261
499
  }
262
500
  recordEvent(name, fields) {
263
- console.info(`[Metrics:Event] ${name}`, fields ?? "");
501
+ this.record("Event", {
502
+ name,
503
+ ...fields
504
+ });
264
505
  }
265
506
  };
266
507
  function makeDependencies(container, options = {}) {
267
- const { fetch: fetchFn = globalThis.fetch?.bind(globalThis), featureFlags = {} } = options;
268
- const loggerFactory = new ConsoleLoggerFactory();
508
+ const { fetch: fetchFn = globalThis.fetch?.bind(globalThis), featureFlags = {}, featureFlagsProviders = [], reportCallback, eventRecorder, locale, platform } = options;
509
+ const consoleFactory = new ConsoleLoggerFactory();
510
+ const loggerFactory = reportCallback ? new CompositeLoggerFactory([consoleFactory, new ReportingLoggerFactory({ report: reportCallback })]) : consoleFactory;
269
511
  container.register(DEP_KEYS.LOGGER_FACTORY, () => loggerFactory);
270
512
  container.register(DEP_KEYS.LOGGER, () => loggerFactory.loggerFor("framework"));
271
513
  container.register(DEP_KEYS.NET, () => ({ fetch: (url, opts) => fetchFn(url, opts) }));
272
514
  container.register(DEP_KEYS.STORAGE, () => new MemoryStorage());
273
- container.register(DEP_KEYS.FEATURE_FLAGS, () => new DefaultFeatureFlags(featureFlags));
515
+ const flags = new DefaultFeatureFlags(featureFlags);
516
+ for (const provider of featureFlagsProviders) flags.addProvider(provider);
517
+ container.register(DEP_KEYS.FEATURE_FLAGS, () => flags);
274
518
  container.register(DEP_KEYS.METRICS, () => new ConsoleMetrics());
519
+ container.register(DEP_KEYS.EVENT_RECORDER, () => eventRecorder ?? new ConsoleEventRecorder());
520
+ if (locale) container.register(DEP_KEYS.LOCALE, () => getLocaleAttributes(locale));
521
+ container.register(DEP_KEYS.PLATFORM, () => platform ?? detectPlatform(typeof navigator !== "undefined" ? navigator.userAgent : void 0));
275
522
  container.register(DEP_KEYS.FETCH, () => fetchFn);
276
523
  }
277
524
  //#endregion
@@ -355,37 +602,6 @@ var Router = class {
355
602
  }
356
603
  };
357
604
  //#endregion
358
- //#region ../core/src/logger/composite.ts
359
- var CompositeLoggerFactory = class {
360
- constructor(factories) {
361
- this.factories = factories;
362
- }
363
- loggerFor(name) {
364
- return new CompositeLogger(this.factories.map((f) => f.loggerFor(name)));
365
- }
366
- };
367
- var CompositeLogger = class {
368
- constructor(loggers) {
369
- this.loggers = loggers;
370
- }
371
- debug(...args) {
372
- return this.callAll("debug", args);
373
- }
374
- info(...args) {
375
- return this.callAll("info", args);
376
- }
377
- warn(...args) {
378
- return this.callAll("warn", args);
379
- }
380
- error(...args) {
381
- return this.callAll("error", args);
382
- }
383
- callAll(method, args) {
384
- for (const logger of this.loggers) logger[method](...args);
385
- return "";
386
- }
387
- };
388
- //#endregion
389
605
  //#region ../core/src/middleware/pipeline.ts
390
606
  /** 执行 beforeLoad 守卫链 */
391
607
  async function runBeforeLoadGuards(guards, ctx) {
@@ -541,6 +757,14 @@ var Framework = class Framework {
541
757
  title: page.title
542
758
  });
543
759
  }
760
+ /** 获取 locale 信息(如果已配置) */
761
+ getLocale() {
762
+ return this.container.has(DEP_KEYS.LOCALE) ? this.container.resolve(DEP_KEYS.LOCALE) : void 0;
763
+ }
764
+ /** 获取平台信息 */
765
+ getPlatform() {
766
+ return this.container.resolve(DEP_KEYS.PLATFORM);
767
+ }
544
768
  /** 注册 Action 处理器 */
545
769
  onAction(kind, handler) {
546
770
  this.actionDispatcher.onAction(kind, handler);
@@ -606,10 +830,24 @@ var HttpClient = class {
606
830
  baseUrl;
607
831
  defaultHeaders;
608
832
  fetchFn;
833
+ requestInterceptors;
834
+ responseInterceptors;
609
835
  constructor(config) {
610
836
  this.baseUrl = config.baseUrl;
611
837
  this.defaultHeaders = config.defaultHeaders ?? {};
612
838
  this.fetchFn = config.fetch ?? globalThis.fetch.bind(globalThis);
839
+ this.requestInterceptors = [...config.requestInterceptors ?? []];
840
+ this.responseInterceptors = [...config.responseInterceptors ?? []];
841
+ }
842
+ /** 动态添加请求拦截器 */
843
+ useRequestInterceptor(interceptor) {
844
+ this.requestInterceptors.push(interceptor);
845
+ return this;
846
+ }
847
+ /** 动态添加响应拦截器 */
848
+ useResponseInterceptor(interceptor) {
849
+ this.responseInterceptors.push(interceptor);
850
+ return this;
613
851
  }
614
852
  /** GET 请求,返回解析后的 JSON */
615
853
  async get(path, params) {
@@ -649,7 +887,7 @@ var HttpClient = class {
649
887
  ...this.defaultHeaders,
650
888
  ...options?.headers
651
889
  };
652
- const init = {
890
+ let init = {
653
891
  method,
654
892
  headers
655
893
  };
@@ -657,7 +895,9 @@ var HttpClient = class {
657
895
  headers["Content-Type"] = headers["Content-Type"] ?? "application/json";
658
896
  init.body = JSON.stringify(options.body);
659
897
  }
660
- const response = await this.fetchFn(url, init);
898
+ for (const interceptor of this.requestInterceptors) init = await interceptor(url, init);
899
+ let response = await this.fetchFn(url, init);
900
+ for (const interceptor of this.responseInterceptors) response = await interceptor(response, url);
661
901
  if (!response.ok) {
662
902
  const body = await response.text().catch(() => void 0);
663
903
  throw new HttpError(response.status, response.statusText, body);
@@ -834,6 +1074,21 @@ function isNone(value) {
834
1074
  return value === null || value === void 0;
835
1075
  }
836
1076
  //#endregion
1077
+ //#region ../core/src/utils/pwa.ts
1078
+ /**
1079
+ * 检测 PWA display mode
1080
+ *
1081
+ * - `standalone`: 已安装的 PWA(通过 Add to Home Screen)
1082
+ * - `twa`: Trusted Web Activity(Android 原生壳)
1083
+ * - `browser`: 普通浏览器标签页
1084
+ */
1085
+ function getPWADisplayMode() {
1086
+ if (typeof window === "undefined") return "browser";
1087
+ if (document.referrer.startsWith("android-app://")) return "twa";
1088
+ if (window.matchMedia("(display-mode: standalone)").matches || "standalone" in window.navigator && window.navigator.standalone === true) return "standalone";
1089
+ return "browser";
1090
+ }
1091
+ //#endregion
837
1092
  //#region ../core/src/utils/url.ts
838
1093
  /**
839
1094
  * URL 工具函数
@@ -953,6 +1208,171 @@ function deny(status = 403, message = "Forbidden") {
953
1208
  };
954
1209
  }
955
1210
  //#endregion
1211
+ //#region ../core/src/metrics/composite-recorder.ts
1212
+ var CompositeEventRecorder = class {
1213
+ recorders;
1214
+ constructor(recorders) {
1215
+ this.recorders = recorders;
1216
+ }
1217
+ record(type, fields) {
1218
+ for (const recorder of this.recorders) recorder.record(type, fields);
1219
+ }
1220
+ async flush() {
1221
+ await Promise.all(this.recorders.map((r) => r.flush?.()));
1222
+ }
1223
+ destroy() {
1224
+ for (const recorder of this.recorders) recorder.destroy?.();
1225
+ }
1226
+ };
1227
+ //#endregion
1228
+ //#region ../core/src/metrics/impression-observer.ts
1229
+ var IntersectionImpressionObserver = class {
1230
+ observer;
1231
+ tracked = /* @__PURE__ */ new Map();
1232
+ captured = [];
1233
+ minDuration;
1234
+ constructor(options = {}) {
1235
+ this.minDuration = options.minVisibleDuration ?? 1e3;
1236
+ this.observer = new IntersectionObserver((entries) => {
1237
+ const now = Date.now();
1238
+ for (const entry of entries) {
1239
+ const tracked = this.tracked.get(entry.target);
1240
+ if (!tracked) continue;
1241
+ if (entry.isIntersecting) {
1242
+ if (tracked.visibleSince === null) tracked.visibleSince = now;
1243
+ } else if (tracked.visibleSince !== null) {
1244
+ if (now - tracked.visibleSince >= this.minDuration) this.captured.push({
1245
+ id: tracked.id,
1246
+ timestamp: tracked.visibleSince,
1247
+ metadata: tracked.metadata
1248
+ });
1249
+ tracked.visibleSince = null;
1250
+ }
1251
+ }
1252
+ }, { threshold: options.threshold ?? .5 });
1253
+ }
1254
+ observe(element, id, metadata) {
1255
+ this.tracked.set(element, {
1256
+ id,
1257
+ metadata,
1258
+ visibleSince: null
1259
+ });
1260
+ this.observer.observe(element);
1261
+ }
1262
+ unobserve(element) {
1263
+ this.observer.unobserve(element);
1264
+ this.tracked.delete(element);
1265
+ }
1266
+ consume() {
1267
+ const now = Date.now();
1268
+ for (const [, tracked] of this.tracked) if (tracked.visibleSince !== null) {
1269
+ if (now - tracked.visibleSince >= this.minDuration) {
1270
+ this.captured.push({
1271
+ id: tracked.id,
1272
+ timestamp: tracked.visibleSince,
1273
+ metadata: tracked.metadata
1274
+ });
1275
+ tracked.visibleSince = now;
1276
+ }
1277
+ }
1278
+ return this.captured.splice(0);
1279
+ }
1280
+ destroy() {
1281
+ this.observer.disconnect();
1282
+ this.tracked.clear();
1283
+ this.captured.length = 0;
1284
+ }
1285
+ };
1286
+ //#endregion
1287
+ //#region ../core/src/metrics/void-recorder.ts
1288
+ var VoidEventRecorder = class {
1289
+ record() {}
1290
+ async flush() {}
1291
+ destroy() {}
1292
+ };
1293
+ //#endregion
1294
+ //#region ../core/src/metrics/with-fields-recorder.ts
1295
+ var WithFieldsRecorder = class {
1296
+ constructor(inner, providers) {
1297
+ this.inner = inner;
1298
+ this.providers = providers;
1299
+ }
1300
+ record(type, fields) {
1301
+ let merged = {};
1302
+ for (const provider of this.providers) Object.assign(merged, provider.getFields());
1303
+ if (fields) Object.assign(merged, fields);
1304
+ this.inner.record(type, merged);
1305
+ }
1306
+ async flush() {
1307
+ return this.inner.flush?.();
1308
+ }
1309
+ destroy() {
1310
+ this.inner.destroy?.();
1311
+ }
1312
+ };
1313
+ //#endregion
1314
+ //#region ../core/src/i18n/interpolate.ts
1315
+ /**
1316
+ * ICU 消息格式插值
1317
+ *
1318
+ * 支持 `{name}` 占位符替换和基础复数规则。
1319
+ */
1320
+ /** 将 `{key}` 占位符替换为 values 中的对应值 */
1321
+ function interpolate(template, values) {
1322
+ if (!values) return template;
1323
+ return template.replace(/\{(\w+)\}/g, (_, key) => {
1324
+ const val = values[key];
1325
+ return val !== void 0 ? String(val) : `{${key}}`;
1326
+ });
1327
+ }
1328
+ /**
1329
+ * 英语复数规则(默认)
1330
+ * 0 → other, 1 → one, 2+ → other
1331
+ */
1332
+ function englishPlural(count) {
1333
+ return count === 1 ? "one" : "other";
1334
+ }
1335
+ /**
1336
+ * 解析带复数后缀的翻译 key
1337
+ *
1338
+ * 约定: `key.one`, `key.other`, `key.zero`, etc.
1339
+ */
1340
+ function resolvePluralKey(key, category) {
1341
+ return `${key}.${category}`;
1342
+ }
1343
+ //#endregion
1344
+ //#region ../core/src/i18n/translator.ts
1345
+ /**
1346
+ * SimpleTranslator — 默认翻译器实现
1347
+ *
1348
+ * 从扁平的 key→string 映射提供翻译,支持 ICU 插值和复数规则。
1349
+ */
1350
+ var SimpleTranslator = class {
1351
+ locale;
1352
+ messages;
1353
+ pluralRule;
1354
+ fallback;
1355
+ constructor(options) {
1356
+ this.locale = options.locale;
1357
+ this.messages = options.messages;
1358
+ this.pluralRule = options.pluralRule ?? englishPlural;
1359
+ this.fallback = options.fallback ?? ((key) => key);
1360
+ }
1361
+ t(key, values) {
1362
+ const template = this.messages[key];
1363
+ if (template === void 0) return this.fallback(key);
1364
+ return interpolate(template, values);
1365
+ }
1366
+ plural(key, count, values) {
1367
+ const pluralKey = resolvePluralKey(key, this.pluralRule(count));
1368
+ const mergedValues = {
1369
+ count,
1370
+ ...values
1371
+ };
1372
+ return this.t(pluralKey, mergedValues);
1373
+ }
1374
+ };
1375
+ //#endregion
956
1376
  //#region ../browser/src/action-handlers/external-url-action.ts
957
1377
  function registerExternalUrlHandler(deps) {
958
1378
  const { framework, log } = deps;
@@ -963,59 +1383,105 @@ function registerExternalUrlHandler(deps) {
963
1383
  }
964
1384
  //#endregion
965
1385
  //#region ../browser/src/utils/try-scroll.ts
966
- /** 最大等待时间 (ms) */
967
1386
  const MAX_WAIT_MS = 5e3;
968
- /** 滚动位置容差 (px) */
969
- const FUDGE = 16;
970
- let teardown = null;
1387
+ const POLL_INTERVAL_MS = 100;
1388
+ const SCROLL_TOLERANCE = 2;
1389
+ let pendingCleanup = null;
1390
+ function cancelTryScroll() {
1391
+ pendingCleanup?.();
1392
+ }
971
1393
  function tryScroll(log, getScrollableElement, scrollY) {
972
- if (teardown) {
973
- teardown();
974
- teardown = null;
975
- }
976
- if (scrollY <= 0) {
977
- const el = getScrollableElement();
978
- if (el) el.scrollTop = 0;
979
- return;
980
- }
981
- const el = getScrollableElement();
982
- if (!el) {
983
- log.warn("could not restore scroll: scrollable element missing");
984
- return;
1394
+ cancelTryScroll();
1395
+ const target = Math.max(0, scrollY);
1396
+ const startedAt = Date.now();
1397
+ let disposed = false;
1398
+ let pendingFrame = null;
1399
+ let intervalId = null;
1400
+ let timeoutId = null;
1401
+ let mutationObserver = null;
1402
+ pendingCleanup = cleanup;
1403
+ observeDocumentActivity();
1404
+ document.addEventListener("load", scheduleAttempt, true);
1405
+ intervalId = setInterval(scheduleAttempt, POLL_INTERVAL_MS);
1406
+ timeoutId = setTimeout(scheduleAttempt, MAX_WAIT_MS);
1407
+ scheduleAttempt();
1408
+ function scheduleAttempt() {
1409
+ if (disposed || pendingFrame !== null) return;
1410
+ pendingFrame = requestAnimationFrame(() => {
1411
+ pendingFrame = null;
1412
+ attemptRestore();
1413
+ });
985
1414
  }
986
- function attemptScroll() {
987
- if (!el) return false;
988
- if (scrollY + el.offsetHeight <= el.scrollHeight + FUDGE) {
989
- el.scrollTop = scrollY;
990
- log.info("scroll restored to", scrollY);
991
- return true;
1415
+ function attemptRestore() {
1416
+ if (disposed) return;
1417
+ const elapsedMs = Date.now() - startedAt;
1418
+ const element = getScrollableElement();
1419
+ if (!element) {
1420
+ if (elapsedMs >= MAX_WAIT_MS) {
1421
+ log.warn("tryScroll: timed out waiting for the scrollable element", {
1422
+ target,
1423
+ elapsedMs
1424
+ });
1425
+ cleanup();
1426
+ }
1427
+ return;
1428
+ }
1429
+ element.scrollTop = target;
1430
+ const actual = element.scrollTop;
1431
+ if (actual >= target - SCROLL_TOLERANCE) {
1432
+ log.info("scroll restored", {
1433
+ target,
1434
+ actual,
1435
+ elapsedMs
1436
+ });
1437
+ cleanup();
1438
+ return;
1439
+ }
1440
+ if (elapsedMs >= MAX_WAIT_MS) {
1441
+ log.warn("tryScroll: timed out before reaching the target", {
1442
+ target,
1443
+ actual,
1444
+ elapsedMs,
1445
+ scrollHeight: element.scrollHeight,
1446
+ clientHeight: element.clientHeight
1447
+ });
1448
+ cleanup();
992
1449
  }
993
- return false;
994
1450
  }
995
- if (attemptScroll()) return;
996
- let resizeObserver = null;
997
- let timeoutId = null;
1451
+ function observeDocumentActivity() {
1452
+ if (typeof MutationObserver === "undefined") return;
1453
+ const root = document.body ?? document.documentElement;
1454
+ if (!root) return;
1455
+ mutationObserver = new MutationObserver(() => {
1456
+ scheduleAttempt();
1457
+ });
1458
+ mutationObserver.observe(root, {
1459
+ childList: true,
1460
+ subtree: true,
1461
+ characterData: true,
1462
+ attributes: true
1463
+ });
1464
+ }
998
1465
  function cleanup() {
999
- if (resizeObserver) {
1000
- resizeObserver.disconnect();
1001
- resizeObserver = null;
1466
+ if (disposed) return;
1467
+ disposed = true;
1468
+ if (pendingCleanup === cleanup) pendingCleanup = null;
1469
+ if (pendingFrame !== null) {
1470
+ cancelAnimationFrame(pendingFrame);
1471
+ pendingFrame = null;
1472
+ }
1473
+ if (intervalId !== null) {
1474
+ clearInterval(intervalId);
1475
+ intervalId = null;
1002
1476
  }
1003
1477
  if (timeoutId !== null) {
1004
1478
  clearTimeout(timeoutId);
1005
1479
  timeoutId = null;
1006
1480
  }
1007
- teardown = null;
1481
+ mutationObserver?.disconnect();
1482
+ mutationObserver = null;
1483
+ document.removeEventListener("load", scheduleAttempt, true);
1008
1484
  }
1009
- resizeObserver = new ResizeObserver(() => {
1010
- if (attemptScroll()) cleanup();
1011
- });
1012
- resizeObserver.observe(el);
1013
- timeoutId = setTimeout(() => {
1014
- log.warn(`tryScroll: timed out after ${MAX_WAIT_MS}ms, target=${scrollY}, scrollHeight=${el.scrollHeight}`);
1015
- el.scrollTop = el.scrollHeight;
1016
- cleanup();
1017
- }, MAX_WAIT_MS);
1018
- teardown = cleanup;
1019
1485
  }
1020
1486
  //#endregion
1021
1487
  //#region ../browser/src/utils/history.ts
@@ -1031,6 +1497,7 @@ var History = class {
1031
1497
  this.getScrollablePageElement = options.getScrollablePageElement;
1032
1498
  }
1033
1499
  replaceState(state, url) {
1500
+ cancelTryScroll();
1034
1501
  const id = generateUuid();
1035
1502
  window.history.replaceState({ id }, "", url);
1036
1503
  this.currentStateId = id;
@@ -1042,6 +1509,7 @@ var History = class {
1042
1509
  this.log.info("replaceState", state, url, id);
1043
1510
  }
1044
1511
  pushState(state, url) {
1512
+ cancelTryScroll();
1045
1513
  const id = generateUuid();
1046
1514
  window.history.pushState({ id }, "", url);
1047
1515
  this.currentStateId = id;
@@ -1053,6 +1521,7 @@ var History = class {
1053
1521
  this.log.info("pushState", state, url, id);
1054
1522
  }
1055
1523
  beforeTransition() {
1524
+ cancelTryScroll();
1056
1525
  const { state } = window.history;
1057
1526
  if (!state) return;
1058
1527
  const oldEntry = this.entries.get(state.id);
@@ -1069,11 +1538,12 @@ var History = class {
1069
1538
  }
1070
1539
  onPopState(listener) {
1071
1540
  window.addEventListener("popstate", async (event) => {
1541
+ cancelTryScroll();
1072
1542
  this.currentStateId = event.state?.id;
1073
1543
  if (!this.currentStateId) this.log.warn("encountered a null event.state.id in onPopState event:", window.location.href);
1074
1544
  this.log.info("popstate", this.entries, this.currentStateId);
1075
1545
  const entry = this.currentStateId ? this.entries.get(this.currentStateId) : void 0;
1076
- await listener(window.location.href, entry?.state);
1546
+ listener(window.location.href, entry?.state);
1077
1547
  if (!entry) return;
1078
1548
  const { scrollY } = entry;
1079
1549
  this.log.info("restoring scroll to", scrollY);
@@ -1082,6 +1552,7 @@ var History = class {
1082
1552
  }
1083
1553
  /** 仅推入 URL,不缓存页面状态(用于页面加载失败场景) */
1084
1554
  pushUrl(url) {
1555
+ cancelTryScroll();
1085
1556
  const id = generateUuid();
1086
1557
  window.history.pushState({ id }, "", url);
1087
1558
  this.currentStateId = id;
@@ -1090,6 +1561,7 @@ var History = class {
1090
1561
  }
1091
1562
  /** 仅替换 URL,不缓存页面状态(用于页面加载失败场景) */
1092
1563
  replaceUrl(url) {
1564
+ cancelTryScroll();
1093
1565
  const id = generateUuid();
1094
1566
  window.history.replaceState({ id }, "", url);
1095
1567
  this.currentStateId = id;
@@ -1343,11 +1815,20 @@ function createPrefetchedIntentsFromDom() {
1343
1815
  * 自动执行 hydration 全流程。
1344
1816
  */
1345
1817
  async function startBrowserApp(config) {
1346
- const { bootstrap, mountId = "app", mount, callbacks } = config;
1818
+ const { bootstrap, mountId = "app", mount, callbacks, onBeforeStart, onAfterStart, frameworkConfig } = config;
1347
1819
  const prefetchedIntents = createPrefetchedIntentsFromDom();
1348
- const framework = Framework.create({ prefetchedIntents });
1820
+ const framework = Framework.create({
1821
+ ...frameworkConfig,
1822
+ prefetchedIntents
1823
+ });
1349
1824
  bootstrap(framework);
1350
1825
  const log = framework.container.resolve(DEP_KEYS.LOGGER_FACTORY).loggerFor("browser");
1826
+ const locale = framework.getLocale();
1827
+ if (locale) {
1828
+ setHtmlLocaleAttributes(locale);
1829
+ log.debug("[startBrowserApp] Applied locale attributes:", locale);
1830
+ }
1831
+ await onBeforeStart?.(framework);
1351
1832
  const initialAction = framework.routeUrl(window.location.pathname + window.location.search);
1352
1833
  const target = document.getElementById(mountId);
1353
1834
  if (!target) throw new Error(`[startBrowserApp] Mount target not found: #${mountId}. Ensure your HTML has <div id="${mountId}"></div>.`);
@@ -1364,6 +1845,7 @@ async function startBrowserApp(config) {
1364
1845
  page: Promise.reject(/* @__PURE__ */ new Error("404")),
1365
1846
  isFirstPage: true
1366
1847
  });
1848
+ await onAfterStart?.(framework);
1367
1849
  }
1368
1850
  //#endregion
1369
1851
  //#region ../ssr/src/render.ts
@@ -1376,7 +1858,7 @@ async function startBrowserApp(config) {
1376
1858
  * 4. 调用应用层提供的渲染函数
1377
1859
  */
1378
1860
  async function ssrRender(options) {
1379
- const { url, frameworkConfig, bootstrap, getErrorPage, renderApp, ssrContext } = options;
1861
+ const { url, frameworkConfig, bootstrap, getErrorPage, renderApp, ssrContext, resolveLocale } = options;
1380
1862
  const mergedConfig = ssrContext?.fetch ? {
1381
1863
  ...frameworkConfig,
1382
1864
  fetch: ssrContext.fetch
@@ -1429,13 +1911,15 @@ async function ssrRender(options) {
1429
1911
  }
1430
1912
  } else page = getErrorPage(404, "Page not found");
1431
1913
  const result = await renderApp(page, framework);
1914
+ const locale = resolveLocale?.(url, ssrContext?.request) ?? framework.getLocale();
1432
1915
  return {
1433
1916
  html: result.html,
1434
1917
  head: result.head,
1435
1918
  css: result.css,
1436
1919
  serverData,
1437
1920
  renderMode: match?.renderMode,
1438
- slots: result.slots
1921
+ slots: result.slots,
1922
+ locale
1439
1923
  };
1440
1924
  } finally {
1441
1925
  framework.dispose();
@@ -1512,14 +1996,18 @@ const SSR_PLACEHOLDERS = {
1512
1996
  /** 匹配所有 <!--ssr-xxx--> 占位符(含内置与自定义) */
1513
1997
  const PLACEHOLDER_REGEX = /<!--ssr-([a-z][a-z0-9-]*)-->/g;
1514
1998
  function injectSSRContent(options) {
1515
- const { template, head, css, html, serializedData, slots } = options;
1999
+ const { template, head, css, html, serializedData, slots, locale } = options;
1516
2000
  const replacements = {
1517
2001
  head: `${head}\n${css ? `<style>${css}</style>` : ""}`,
1518
2002
  body: html,
1519
2003
  data: `<script id="serialized-server-data" type="application/json">${serializedData}<\/script>`,
1520
2004
  ...slots
1521
2005
  };
1522
- return template.replace(PLACEHOLDER_REGEX, (_, name) => replacements[name] ?? "");
2006
+ let result = template.replace(PLACEHOLDER_REGEX, (_, name) => replacements[name] ?? "");
2007
+ if (locale) result = result.replace(/(<html)([^>]*)(>)/i, (match, open, attrs, close) => {
2008
+ return `${open}${attrs.replace(/\s+lang="[^"]*"/gi, "").replace(/\s+dir="[^"]*"/gi, "")} lang="${locale.lang}" dir="${locale.dir}"${close}`;
2009
+ });
2010
+ return result;
1523
2011
  }
1524
2012
  /**
1525
2013
  * CSR 空壳注入 — 清空所有占位符
@@ -2391,13 +2879,13 @@ function autoAdapter() {
2391
2879
  return {
2392
2880
  name: "auto",
2393
2881
  async build(ctx) {
2394
- const detected = detectPlatform();
2882
+ const detected = detectPlatform$1();
2395
2883
  console.log(` [auto] Detected platform: ${detected}\n`);
2396
2884
  return resolveAdapter(detected).build(ctx);
2397
2885
  }
2398
2886
  };
2399
2887
  }
2400
- function detectPlatform() {
2888
+ function detectPlatform$1() {
2401
2889
  if (process.env.VERCEL) return "vercel";
2402
2890
  if (process.env.CF_PAGES) return "cloudflare";
2403
2891
  if (process.env.NETLIFY) return "netlify";
@@ -2968,6 +3456,6 @@ function finesoftFrontViteConfig(options = {}) {
2968
3456
  };
2969
3457
  }
2970
3458
  //#endregion
2971
- export { ACTION_KINDS, ActionDispatcher, BaseController, BaseLogger, CompositeLogger, CompositeLoggerFactory, ConsoleLogger, ConsoleLoggerFactory, Container, DEP_KEYS, Framework, History, HttpClient, HttpError, IntentDispatcher, LruMap, PrefetchedIntents, Router, SSR_PLACEHOLDERS, autoAdapter, buildUrl, cloudflareAdapter, createBrowserContext, createPrefetchedIntentsFromDom, createSSRApp, createSSRRender, createServer, createServerContext, defineRoutes, deny, deserializeServerData, detectRuntime, finesoftFrontViteConfig, generateProxyCode, generateUuid, getBaseUrl, injectCSRShell, injectSSRContent, isCompoundAction, isExternalUrlAction, isFlowAction, isNone, isSome, makeDependencies, makeExternalUrlAction, makeFlowAction, mapEach, netlifyAdapter, next, nodeAdapter, parseAcceptLanguage, pipe, pipeAsync, redirect, registerActionHandlers, registerExternalUrlHandler, registerFlowActionHandler, registerProxyRoutes, removeHost, removeQueryParams, removeScheme, resetFilterCache, resolveAdapter, resolveRoot, rewrite, runAfterLoadGuards, runBeforeLoadGuards, serializeServerData, shouldLog, ssrRender, stableStringify, startBrowserApp, startServer, staticAdapter, tryScroll, vercelAdapter };
3459
+ export { ACTION_KINDS, ActionDispatcher, BaseController, BaseLogger, CompositeEventRecorder, CompositeLogger, CompositeLoggerFactory, ConsoleEventRecorder, ConsoleLogger, ConsoleLoggerFactory, Container, DEP_KEYS, Framework, History, HttpClient, HttpError, IntentDispatcher, IntersectionImpressionObserver, LruMap, PrefetchedIntents, ReportingLogger, ReportingLoggerFactory, Router, SSR_PLACEHOLDERS, SimpleTranslator, VoidEventRecorder, WithFieldsRecorder, autoAdapter, buildUrl, cloudflareAdapter, createBrowserContext, createPrefetchedIntentsFromDom, createSSRApp, createSSRRender, createServer, createServerContext, defineRoutes, deny, deserializeServerData, detectPlatform, detectRuntime, englishPlural, finesoftFrontViteConfig, generateProxyCode, generateUuid, getBaseUrl, getLocaleAttributes, getPWADisplayMode, getTextDirection, injectCSRShell, injectSSRContent, interpolate, isCompoundAction, isExternalUrlAction, isFlowAction, isNone, isRtl, isSome, makeDependencies, makeExternalUrlAction, makeFlowAction, makeLocaleInfo, mapEach, netlifyAdapter, next, nodeAdapter, parseAcceptLanguage, pipe, pipeAsync, redirect, registerActionHandlers, registerExternalUrlHandler, registerFlowActionHandler, registerProxyRoutes, removeHost, removeQueryParams, removeScheme, resetFilterCache, resolveAdapter, resolvePluralKey, resolveRoot, rewrite, runAfterLoadGuards, runBeforeLoadGuards, serializeServerData, setHtmlLocaleAttributes, shouldLog, ssrRender, stableStringify, startBrowserApp, startServer, staticAdapter, tryScroll, vercelAdapter };
2972
3460
 
2973
3461
  //# sourceMappingURL=index.mjs.map