@finesoft/front 0.1.54 → 0.1.56
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.d.mts +101 -4
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +213 -100
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -203,6 +203,95 @@ function setHtmlLocaleAttributes(attrs) {
|
|
|
203
203
|
document.documentElement.lang = attrs.lang;
|
|
204
204
|
document.documentElement.dir = attrs.dir;
|
|
205
205
|
}
|
|
206
|
+
/**
|
|
207
|
+
* 从 URL 前缀中提取 locale
|
|
208
|
+
*
|
|
209
|
+
* @param url - 请求 URL(如 "/zh/about")
|
|
210
|
+
* @param supportedLocales - 支持的 locale 列表(如 ["zh", "en", "ja"])
|
|
211
|
+
* @returns 匹配时返回 `{ locale, strippedUrl }`,不匹配返回 null
|
|
212
|
+
*
|
|
213
|
+
* @example
|
|
214
|
+
* ```ts
|
|
215
|
+
* resolveLocaleFromUrl("/zh/about", ["zh", "en"])
|
|
216
|
+
* // → { locale: "zh", strippedUrl: "/about" }
|
|
217
|
+
*
|
|
218
|
+
* resolveLocaleFromUrl("/about", ["zh", "en"])
|
|
219
|
+
* // → null
|
|
220
|
+
* ```
|
|
221
|
+
*/
|
|
222
|
+
function resolveLocaleFromUrl(url, supportedLocales) {
|
|
223
|
+
const match = url.split("?")[0].match(/^\/([^/]+)(\/.*)?$/);
|
|
224
|
+
if (!match) return null;
|
|
225
|
+
const candidate = match[1];
|
|
226
|
+
const found = supportedLocales.find((l) => l.toLowerCase() === candidate.toLowerCase());
|
|
227
|
+
if (!found) return null;
|
|
228
|
+
return {
|
|
229
|
+
locale: found,
|
|
230
|
+
strippedUrl: match[2] || "/"
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
//#endregion
|
|
234
|
+
//#region ../core/src/i18n/interpolate.ts
|
|
235
|
+
/**
|
|
236
|
+
* ICU 消息格式插值
|
|
237
|
+
*
|
|
238
|
+
* 支持 `{name}` 占位符替换和基础复数规则。
|
|
239
|
+
*/
|
|
240
|
+
/** 将 `{key}` 占位符替换为 values 中的对应值 */
|
|
241
|
+
function interpolate(template, values) {
|
|
242
|
+
if (!values) return template;
|
|
243
|
+
return template.replace(/\{(\w+)\}/g, (_, key) => {
|
|
244
|
+
const val = values[key];
|
|
245
|
+
return val !== void 0 ? String(val) : `{${key}}`;
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* 英语复数规则(默认)
|
|
250
|
+
* 0 → other, 1 → one, 2+ → other
|
|
251
|
+
*/
|
|
252
|
+
function englishPlural(count) {
|
|
253
|
+
return count === 1 ? "one" : "other";
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* 解析带复数后缀的翻译 key
|
|
257
|
+
*
|
|
258
|
+
* 约定: `key.one`, `key.other`, `key.zero`, etc.
|
|
259
|
+
*/
|
|
260
|
+
function resolvePluralKey(key, category) {
|
|
261
|
+
return `${key}.${category}`;
|
|
262
|
+
}
|
|
263
|
+
//#endregion
|
|
264
|
+
//#region ../core/src/i18n/translator.ts
|
|
265
|
+
/**
|
|
266
|
+
* SimpleTranslator — 默认翻译器实现
|
|
267
|
+
*
|
|
268
|
+
* 从扁平的 key→string 映射提供翻译,支持 ICU 插值和复数规则。
|
|
269
|
+
*/
|
|
270
|
+
var SimpleTranslator = class {
|
|
271
|
+
locale;
|
|
272
|
+
messages;
|
|
273
|
+
pluralRule;
|
|
274
|
+
fallback;
|
|
275
|
+
constructor(options) {
|
|
276
|
+
this.locale = options.locale;
|
|
277
|
+
this.messages = options.messages;
|
|
278
|
+
this.pluralRule = options.pluralRule ?? englishPlural;
|
|
279
|
+
this.fallback = options.fallback ?? ((key) => key);
|
|
280
|
+
}
|
|
281
|
+
t(key, values) {
|
|
282
|
+
const template = this.messages[key];
|
|
283
|
+
if (template === void 0) return this.fallback(key);
|
|
284
|
+
return interpolate(template, values);
|
|
285
|
+
}
|
|
286
|
+
plural(key, count, values) {
|
|
287
|
+
const pluralKey = resolvePluralKey(key, this.pluralRule(count));
|
|
288
|
+
const mergedValues = {
|
|
289
|
+
count,
|
|
290
|
+
...values
|
|
291
|
+
};
|
|
292
|
+
return this.t(pluralKey, mergedValues);
|
|
293
|
+
}
|
|
294
|
+
};
|
|
206
295
|
//#endregion
|
|
207
296
|
//#region ../core/src/logger/composite.ts
|
|
208
297
|
var CompositeLoggerFactory = class {
|
|
@@ -442,7 +531,8 @@ const DEP_KEYS = {
|
|
|
442
531
|
FETCH: "fetch",
|
|
443
532
|
EVENT_RECORDER: "eventRecorder",
|
|
444
533
|
LOCALE: "locale",
|
|
445
|
-
PLATFORM: "platform"
|
|
534
|
+
PLATFORM: "platform",
|
|
535
|
+
TRANSLATOR: "translator"
|
|
446
536
|
};
|
|
447
537
|
var MemoryStorage = class {
|
|
448
538
|
store = /* @__PURE__ */ new Map();
|
|
@@ -504,8 +594,25 @@ var ConsoleMetrics = class {
|
|
|
504
594
|
});
|
|
505
595
|
}
|
|
506
596
|
};
|
|
597
|
+
/**
|
|
598
|
+
* 从 TranslationMessages 中解析出 SimpleTranslator 所需的扁平 Record<string, string>
|
|
599
|
+
*
|
|
600
|
+
* - 扁平格式:直接返回
|
|
601
|
+
* - 嵌套格式:提取 locale 对应子表并展平复数 key(`{key}.{plural}` 拼接)
|
|
602
|
+
*/
|
|
603
|
+
function resolveMessages(messages, locale) {
|
|
604
|
+
const entries = Object.entries(messages);
|
|
605
|
+
if (entries.length === 0) return void 0;
|
|
606
|
+
if (typeof entries[0][1] === "string") return messages;
|
|
607
|
+
const localeMsgs = messages[locale];
|
|
608
|
+
if (!localeMsgs) return void 0;
|
|
609
|
+
const flat = {};
|
|
610
|
+
for (const [key, value] of Object.entries(localeMsgs)) if (typeof value === "string") flat[key] = value;
|
|
611
|
+
else for (const [suffix, text] of Object.entries(value)) flat[`${key}.${suffix}`] = text;
|
|
612
|
+
return flat;
|
|
613
|
+
}
|
|
507
614
|
function makeDependencies(container, options = {}) {
|
|
508
|
-
const { fetch: fetchFn = globalThis.fetch?.bind(globalThis), featureFlags = {}, featureFlagsProviders = [], reportCallback, eventRecorder, locale, platform } = options;
|
|
615
|
+
const { fetch: fetchFn = globalThis.fetch?.bind(globalThis), featureFlags = {}, featureFlagsProviders = [], reportCallback, eventRecorder, locale, platform, messages } = options;
|
|
509
616
|
const consoleFactory = new ConsoleLoggerFactory();
|
|
510
617
|
const loggerFactory = reportCallback ? new CompositeLoggerFactory([consoleFactory, new ReportingLoggerFactory({ report: reportCallback })]) : consoleFactory;
|
|
511
618
|
container.register(DEP_KEYS.LOGGER_FACTORY, () => loggerFactory);
|
|
@@ -518,6 +625,13 @@ function makeDependencies(container, options = {}) {
|
|
|
518
625
|
container.register(DEP_KEYS.METRICS, () => new ConsoleMetrics());
|
|
519
626
|
container.register(DEP_KEYS.EVENT_RECORDER, () => eventRecorder ?? new ConsoleEventRecorder());
|
|
520
627
|
if (locale) container.register(DEP_KEYS.LOCALE, () => getLocaleAttributes(locale));
|
|
628
|
+
if (locale && messages) {
|
|
629
|
+
const flat = resolveMessages(messages, locale);
|
|
630
|
+
if (flat) container.register(DEP_KEYS.TRANSLATOR, () => new SimpleTranslator({
|
|
631
|
+
locale,
|
|
632
|
+
messages: flat
|
|
633
|
+
}));
|
|
634
|
+
}
|
|
521
635
|
container.register(DEP_KEYS.PLATFORM, () => platform ?? detectPlatform(typeof navigator !== "undefined" ? navigator.userAgent : void 0));
|
|
522
636
|
container.register(DEP_KEYS.FETCH, () => fetchFn);
|
|
523
637
|
}
|
|
@@ -761,6 +875,10 @@ var Framework = class Framework {
|
|
|
761
875
|
getLocale() {
|
|
762
876
|
return this.container.has(DEP_KEYS.LOCALE) ? this.container.resolve(DEP_KEYS.LOCALE) : void 0;
|
|
763
877
|
}
|
|
878
|
+
/** 获取翻译器(如果已通过 messages + locale 配置) */
|
|
879
|
+
getTranslator() {
|
|
880
|
+
return this.container.has(DEP_KEYS.TRANSLATOR) ? this.container.resolve(DEP_KEYS.TRANSLATOR) : void 0;
|
|
881
|
+
}
|
|
764
882
|
/** 获取平台信息 */
|
|
765
883
|
getPlatform() {
|
|
766
884
|
return this.container.resolve(DEP_KEYS.PLATFORM);
|
|
@@ -1010,18 +1128,23 @@ function mapEach(mapper) {
|
|
|
1010
1128
|
* ]);
|
|
1011
1129
|
* ```
|
|
1012
1130
|
*/
|
|
1013
|
-
function defineRoutes(framework, definitions) {
|
|
1131
|
+
function defineRoutes(framework, definitions, options) {
|
|
1014
1132
|
const registeredIntents = /* @__PURE__ */ new Set();
|
|
1015
1133
|
for (const def of definitions) {
|
|
1016
1134
|
if (def.controller && !registeredIntents.has(def.intentId)) {
|
|
1017
1135
|
framework.registerIntent(def.controller);
|
|
1018
1136
|
registeredIntents.add(def.intentId);
|
|
1019
1137
|
}
|
|
1020
|
-
|
|
1138
|
+
const routeOpts = {
|
|
1021
1139
|
renderMode: def.renderMode,
|
|
1022
1140
|
beforeGuards: def.beforeLoad,
|
|
1023
1141
|
afterGuards: def.afterLoad
|
|
1024
|
-
}
|
|
1142
|
+
};
|
|
1143
|
+
framework.router.add(def.path, def.intentId, routeOpts);
|
|
1144
|
+
if (options?.locales?.length) {
|
|
1145
|
+
const localePath = def.path === "/" ? "/:locale" : `/:locale${def.path}`;
|
|
1146
|
+
framework.router.add(localePath, def.intentId, routeOpts);
|
|
1147
|
+
}
|
|
1025
1148
|
}
|
|
1026
1149
|
}
|
|
1027
1150
|
//#endregion
|
|
@@ -1311,68 +1434,6 @@ var WithFieldsRecorder = class {
|
|
|
1311
1434
|
}
|
|
1312
1435
|
};
|
|
1313
1436
|
//#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
|
|
1376
1437
|
//#region ../browser/src/action-handlers/external-url-action.ts
|
|
1377
1438
|
function registerExternalUrlHandler(deps) {
|
|
1378
1439
|
const { framework, log } = deps;
|
|
@@ -1859,10 +1920,15 @@ async function startBrowserApp(config) {
|
|
|
1859
1920
|
*/
|
|
1860
1921
|
async function ssrRender(options) {
|
|
1861
1922
|
const { url, frameworkConfig, bootstrap, getErrorPage, renderApp, ssrContext, resolveLocale } = options;
|
|
1862
|
-
const
|
|
1923
|
+
const resolvedLocale = resolveLocale?.(url, ssrContext?.request);
|
|
1924
|
+
const effectiveConfig = resolvedLocale ? {
|
|
1863
1925
|
...frameworkConfig,
|
|
1864
|
-
|
|
1926
|
+
locale: resolvedLocale.lang
|
|
1865
1927
|
} : frameworkConfig;
|
|
1928
|
+
const mergedConfig = ssrContext?.fetch ? {
|
|
1929
|
+
...effectiveConfig,
|
|
1930
|
+
fetch: ssrContext.fetch
|
|
1931
|
+
} : effectiveConfig;
|
|
1866
1932
|
const framework = Framework.create(mergedConfig);
|
|
1867
1933
|
bootstrap(framework);
|
|
1868
1934
|
try {
|
|
@@ -1911,7 +1977,7 @@ async function ssrRender(options) {
|
|
|
1911
1977
|
}
|
|
1912
1978
|
} else page = getErrorPage(404, "Page not found");
|
|
1913
1979
|
const result = await renderApp(page, framework);
|
|
1914
|
-
const locale =
|
|
1980
|
+
const locale = resolvedLocale ?? framework.getLocale();
|
|
1915
1981
|
return {
|
|
1916
1982
|
html: result.html,
|
|
1917
1983
|
head: result.head,
|
|
@@ -1972,14 +2038,15 @@ async function handleMiddlewareResult(result, getErrorPage, renderApp, framework
|
|
|
1972
2038
|
* @returns `render(url, ssrContext?)` — 供 @finesoft/server SSRModule 使用
|
|
1973
2039
|
*/
|
|
1974
2040
|
function createSSRRender(config) {
|
|
1975
|
-
const { bootstrap, getErrorPage, renderApp, frameworkConfig } = config;
|
|
2041
|
+
const { bootstrap, getErrorPage, renderApp, frameworkConfig, resolveLocale } = config;
|
|
1976
2042
|
return (url, ssrContext) => ssrRender({
|
|
1977
2043
|
url,
|
|
1978
2044
|
frameworkConfig: frameworkConfig ?? {},
|
|
1979
2045
|
bootstrap,
|
|
1980
2046
|
getErrorPage,
|
|
1981
|
-
renderApp: (page) => renderApp(page),
|
|
1982
|
-
ssrContext
|
|
2047
|
+
renderApp: (page, framework) => renderApp(page, framework),
|
|
2048
|
+
ssrContext,
|
|
2049
|
+
resolveLocale
|
|
1983
2050
|
});
|
|
1984
2051
|
}
|
|
1985
2052
|
//#endregion
|
|
@@ -2004,17 +2071,25 @@ function injectSSRContent(options) {
|
|
|
2004
2071
|
...slots
|
|
2005
2072
|
};
|
|
2006
2073
|
let result = template.replace(PLACEHOLDER_REGEX, (_, name) => replacements[name] ?? "");
|
|
2007
|
-
if (locale) result = result
|
|
2008
|
-
return `${open}${attrs.replace(/\s+lang="[^"]*"/gi, "").replace(/\s+dir="[^"]*"/gi, "")} lang="${locale.lang}" dir="${locale.dir}"${close}`;
|
|
2009
|
-
});
|
|
2074
|
+
if (locale) result = applyLocaleToHtml(result, locale);
|
|
2010
2075
|
return result;
|
|
2011
2076
|
}
|
|
2012
2077
|
/**
|
|
2013
2078
|
* CSR 空壳注入 — 清空所有占位符
|
|
2014
2079
|
* 用于 renderMode === "csr" 的路由
|
|
2080
|
+
*
|
|
2081
|
+
* @param locale - 可选的 locale 属性,注入到 `<html lang="" dir="">`
|
|
2015
2082
|
*/
|
|
2016
|
-
function injectCSRShell(template) {
|
|
2017
|
-
|
|
2083
|
+
function injectCSRShell(template, locale) {
|
|
2084
|
+
let result = template.replace(PLACEHOLDER_REGEX, () => "");
|
|
2085
|
+
if (locale) result = applyLocaleToHtml(result, locale);
|
|
2086
|
+
return result;
|
|
2087
|
+
}
|
|
2088
|
+
/** 将 lang/dir 注入到 <html> 标签 */
|
|
2089
|
+
function applyLocaleToHtml(html, locale) {
|
|
2090
|
+
return html.replace(/(<html)([^>]*)(>)/i, (_match, open, attrs, close) => {
|
|
2091
|
+
return `${open}${attrs.replace(/\s+lang="[^"]*"/gi, "").replace(/\s+dir="[^"]*"/gi, "")} lang="${locale.lang}" dir="${locale.dir}"${close}`;
|
|
2092
|
+
});
|
|
2018
2093
|
}
|
|
2019
2094
|
//#endregion
|
|
2020
2095
|
//#region ../ssr/src/server-data.ts
|
|
@@ -2259,22 +2334,40 @@ ${setupImport}
|
|
|
2259
2334
|
|
|
2260
2335
|
const TEMPLATE = ${JSON.stringify(ctx.templateHtml)};
|
|
2261
2336
|
const RENDER_MODES = ${renderModes};
|
|
2337
|
+
const DEFAULT_LOCALE = ${JSON.stringify(ctx.defaultLocale ?? null)};
|
|
2262
2338
|
${cacheImpl}
|
|
2263
2339
|
|
|
2264
|
-
function injectSSR(t, head, css, html, data) {
|
|
2265
|
-
|
|
2340
|
+
function injectSSR(t, head, css, html, data, locale) {
|
|
2341
|
+
const injected = t
|
|
2266
2342
|
.replace(/<!--ssr-([a-z][a-z0-9-]*)-->/g, (_, name) => {
|
|
2267
2343
|
const replacements = {
|
|
2268
|
-
head: head + "
|
|
2344
|
+
head: head + "\n<style>" + css + "</style>",
|
|
2269
2345
|
body: html,
|
|
2270
2346
|
data: '<script id="serialized-server-data" type="application/json">' + data + "<\/script>",
|
|
2271
2347
|
};
|
|
2272
2348
|
return replacements[name] ?? "";
|
|
2273
2349
|
});
|
|
2350
|
+
return applyLocaleToHtml(injected, locale);
|
|
2351
|
+
}
|
|
2352
|
+
|
|
2353
|
+
function applyLocaleToHtml(html, locale) {
|
|
2354
|
+
if (!locale) return html;
|
|
2355
|
+
return html.replace(/<html([^>]*)>/, (_, attrs) => {
|
|
2356
|
+
let a = attrs.replace(/s*lang="[^"]*"/g, "").replace(/s*dir="[^"]*"/g, "");
|
|
2357
|
+
return "<html" + a + ' lang="' + locale.lang + '" dir="' + locale.dir + '">';
|
|
2358
|
+
});
|
|
2359
|
+
}
|
|
2360
|
+
|
|
2361
|
+
function getLocaleAttrs(lang) {
|
|
2362
|
+
if (!lang) return undefined;
|
|
2363
|
+
const RTL = new Set(["ar","arc","dv","fa","ha","he","khw","ks","ku","ps","ur","yi"]);
|
|
2364
|
+
const base = lang.split(/[-_]/)[0].toLowerCase();
|
|
2365
|
+
return { lang: lang, dir: RTL.has(base) ? "rtl" : "ltr" };
|
|
2274
2366
|
}
|
|
2275
2367
|
|
|
2276
|
-
function injectCSRShell(t) {
|
|
2277
|
-
|
|
2368
|
+
function injectCSRShell(t, locale) {
|
|
2369
|
+
const stripped = t.replace(/<!--ssr-([a-z][a-z0-9-]*)-->/g, () => "");
|
|
2370
|
+
return applyLocaleToHtml(stripped, locale);
|
|
2278
2371
|
}
|
|
2279
2372
|
|
|
2280
2373
|
function matchRenderMode(url) {
|
|
@@ -2323,22 +2416,23 @@ app.get("*", async (c) => {
|
|
|
2323
2416
|
// Vite 配置级别覆盖: CSR 直接返回空壳
|
|
2324
2417
|
const overrideMode = matchRenderMode(url);
|
|
2325
2418
|
if (overrideMode === "csr") {
|
|
2326
|
-
return c.html(injectCSRShell(TEMPLATE));
|
|
2419
|
+
return c.html(injectCSRShell(TEMPLATE, getLocaleAttrs(DEFAULT_LOCALE)));
|
|
2327
2420
|
}
|
|
2328
2421
|
|
|
2329
2422
|
// ISR 缓存命中
|
|
2330
2423
|
const cached = await platformCacheGet(url);
|
|
2331
2424
|
if (cached) return c.html(cached);
|
|
2332
2425
|
|
|
2333
|
-
const { html: appHtml, head, css, serverData, renderMode } = await render(url, { fetch: _createInternalFetch(_ssrDepth + 1) });
|
|
2426
|
+
const { html: appHtml, head, css, serverData, renderMode, locale } = await render(url, { fetch: _createInternalFetch(_ssrDepth + 1) });
|
|
2427
|
+
const localeAttrs = getLocaleAttrs(locale || DEFAULT_LOCALE);
|
|
2334
2428
|
|
|
2335
2429
|
// 路由级 CSR
|
|
2336
2430
|
if (renderMode === "csr") {
|
|
2337
|
-
return c.html(injectCSRShell(TEMPLATE));
|
|
2431
|
+
return c.html(injectCSRShell(TEMPLATE, localeAttrs));
|
|
2338
2432
|
}
|
|
2339
2433
|
|
|
2340
2434
|
const serializedData = serializeServerData(serverData);
|
|
2341
|
-
const finalHtml = injectSSR(TEMPLATE, head, css, appHtml, serializedData);
|
|
2435
|
+
const finalHtml = injectSSR(TEMPLATE, head, css, appHtml, serializedData, localeAttrs);
|
|
2342
2436
|
|
|
2343
2437
|
// Prerender ISR 缓存(包括 Vite 配置覆盖和路由级)
|
|
2344
2438
|
if (renderMode === "prerender" || overrideMode === "prerender") {
|
|
@@ -2414,20 +2508,34 @@ async function prerenderRoutes(ctx) {
|
|
|
2414
2508
|
if (ctx.renderModes) {
|
|
2415
2509
|
for (const [pattern, mode] of Object.entries(ctx.renderModes)) if (mode === "prerender" && !pattern.includes("*") && !pattern.includes(":")) prerenderPaths.add(pattern);
|
|
2416
2510
|
}
|
|
2511
|
+
if (ctx.locales?.length) {
|
|
2512
|
+
const basePaths = [...prerenderPaths];
|
|
2513
|
+
for (const locale of ctx.locales) for (const basePath of basePaths) {
|
|
2514
|
+
const localePath = basePath === "/" ? `/${locale}` : `/${locale}${basePath}`;
|
|
2515
|
+
prerenderPaths.add(localePath);
|
|
2516
|
+
}
|
|
2517
|
+
}
|
|
2417
2518
|
if (prerenderPaths.size === 0) return [];
|
|
2418
2519
|
const ssrPath = pathToFileURL(path.resolve(root, "dist/server/ssr.js")).href;
|
|
2419
2520
|
const ssrModule = await dynamicImport(ssrPath);
|
|
2420
2521
|
const results = [];
|
|
2421
2522
|
for (const url of prerenderPaths) try {
|
|
2422
|
-
const { html: appHtml, head, css, serverData } = await ssrModule.render(url);
|
|
2523
|
+
const { html: appHtml, head, css, serverData, locale } = await ssrModule.render(url);
|
|
2423
2524
|
const serializedData = ssrModule.serializeServerData(serverData);
|
|
2424
|
-
|
|
2525
|
+
let finalHtml = ctx.templateHtml.replace(/<!--ssr-([a-z][a-z0-9-]*)-->/g, (_match, name) => {
|
|
2425
2526
|
return {
|
|
2426
2527
|
head: head + "\n<style>" + css + "</style>",
|
|
2427
2528
|
body: appHtml,
|
|
2428
2529
|
data: "<script id=\"serialized-server-data\" type=\"application/json\">" + serializedData + "<\/script>"
|
|
2429
2530
|
}[name] ?? "";
|
|
2430
2531
|
});
|
|
2532
|
+
if (locale) {
|
|
2533
|
+
const { getLocaleAttributes } = await dynamicImport("@finesoft/core");
|
|
2534
|
+
const attrs = getLocaleAttributes(locale);
|
|
2535
|
+
finalHtml = finalHtml.replace(/<html([^>]*)>/, (_m, a) => {
|
|
2536
|
+
return `<html${a.replace(/\s*lang="[^"]*"/g, "").replace(/\s*dir="[^"]*"/g, "")} lang="${attrs.lang}" dir="${attrs.dir}">`;
|
|
2537
|
+
});
|
|
2538
|
+
}
|
|
2431
2539
|
results.push({
|
|
2432
2540
|
url,
|
|
2433
2541
|
html: finalHtml
|
|
@@ -2944,7 +3052,7 @@ function matchRenderModeOverride(url, renderModes) {
|
|
|
2944
3052
|
return null;
|
|
2945
3053
|
}
|
|
2946
3054
|
function createSSRApp(options) {
|
|
2947
|
-
const { root, vite, isProduction, ssrEntryPath = "/src/ssr.ts", ssrProductionModule, parentFetch, renderModes } = options;
|
|
3055
|
+
const { root, vite, isProduction, ssrEntryPath = "/src/ssr.ts", ssrProductionModule, parentFetch, renderModes, defaultLocale } = options;
|
|
2948
3056
|
const app = new Hono();
|
|
2949
3057
|
/** ISR 内存缓存(prerender 路由首次请求后缓存,LRU 驱逐) */
|
|
2950
3058
|
const ISR_CACHE_MAX = 1e3;
|
|
@@ -2992,22 +3100,23 @@ function createSSRApp(options) {
|
|
|
2992
3100
|
if (typeof ssrMod.render !== "function" || typeof ssrMod.serializeServerData !== "function") throw new Error("[SSR] Module missing required exports: render, serializeServerData");
|
|
2993
3101
|
const { render, serializeServerData } = ssrMod;
|
|
2994
3102
|
const overrideMode = matchRenderModeOverride(url, renderModes);
|
|
2995
|
-
if (overrideMode === "csr") return c.html(injectCSRShell(template));
|
|
3103
|
+
if (overrideMode === "csr") return c.html(injectCSRShell(template, defaultLocale ? getLocaleAttributes(defaultLocale) : void 0));
|
|
2996
3104
|
const cached = isrCache.get(url);
|
|
2997
3105
|
if (cached) return c.html(cached);
|
|
2998
3106
|
const requestFetch = parentFetch ? createInternalFetch(parentFetch, ssrDepth + 1) : void 0;
|
|
2999
3107
|
const ssrContext = { request: c.req.raw };
|
|
3000
3108
|
if (requestFetch) ssrContext.fetch = requestFetch;
|
|
3001
|
-
const { html: appHtml, head, css, serverData, renderMode, redirect: middlewareRedirect, slots } = await render(url, ssrContext);
|
|
3109
|
+
const { html: appHtml, head, css, serverData, renderMode, redirect: middlewareRedirect, slots, locale } = await render(url, ssrContext);
|
|
3002
3110
|
if (middlewareRedirect) return c.redirect(middlewareRedirect.url, middlewareRedirect.status);
|
|
3003
|
-
if (renderMode === "csr") return c.html(injectCSRShell(template));
|
|
3111
|
+
if (renderMode === "csr") return c.html(injectCSRShell(template, locale));
|
|
3004
3112
|
const finalHtml = injectSSRContent({
|
|
3005
3113
|
template,
|
|
3006
3114
|
head,
|
|
3007
3115
|
css,
|
|
3008
3116
|
html: appHtml,
|
|
3009
3117
|
serializedData: serializeServerData(serverData),
|
|
3010
|
-
slots
|
|
3118
|
+
slots,
|
|
3119
|
+
locale
|
|
3011
3120
|
});
|
|
3012
3121
|
if (renderMode === "prerender" || overrideMode === "prerender") isrSet(url, finalHtml);
|
|
3013
3122
|
return c.html(finalHtml);
|
|
@@ -3319,7 +3428,8 @@ function finesoftFrontViteConfig(options = {}) {
|
|
|
3319
3428
|
isProduction: false,
|
|
3320
3429
|
ssrEntryPath: "/" + ssrEntry,
|
|
3321
3430
|
parentFetch: app.fetch.bind(app),
|
|
3322
|
-
renderModes: options.renderModes
|
|
3431
|
+
renderModes: options.renderModes,
|
|
3432
|
+
defaultLocale: options.defaultLocale
|
|
3323
3433
|
});
|
|
3324
3434
|
app.route("/", ssrApp);
|
|
3325
3435
|
const listener = getRequestListener(app.fetch);
|
|
@@ -3363,17 +3473,18 @@ function finesoftFrontViteConfig(options = {}) {
|
|
|
3363
3473
|
const url = c.req.path + (c.req.url.includes("?") ? "?" + c.req.url.split("?")[1] : "");
|
|
3364
3474
|
try {
|
|
3365
3475
|
const overrideMode = matchRenderModeConfig(url, options.renderModes);
|
|
3366
|
-
if (overrideMode === "csr") return c.html(injectCSRShell(template));
|
|
3476
|
+
if (overrideMode === "csr") return c.html(injectCSRShell(template, options.defaultLocale ? getLocaleAttributes(options.defaultLocale) : void 0));
|
|
3367
3477
|
const cached = isrCache.get(url);
|
|
3368
3478
|
if (cached) return c.html(cached);
|
|
3369
|
-
const { html: appHtml, head, css, serverData, renderMode } = await ssrModule.render(url, { fetch: createInternalFetch(app.fetch.bind(app), ssrDepth + 1) });
|
|
3370
|
-
if (renderMode === "csr") return c.html(injectCSRShell(template));
|
|
3479
|
+
const { html: appHtml, head, css, serverData, renderMode, locale } = await ssrModule.render(url, { fetch: createInternalFetch(app.fetch.bind(app), ssrDepth + 1) });
|
|
3480
|
+
if (renderMode === "csr") return c.html(injectCSRShell(template, locale));
|
|
3371
3481
|
const finalHtml = injectSSRContent({
|
|
3372
3482
|
template,
|
|
3373
3483
|
head,
|
|
3374
3484
|
css,
|
|
3375
3485
|
html: appHtml,
|
|
3376
|
-
serializedData: ssrModule.serializeServerData(serverData)
|
|
3486
|
+
serializedData: ssrModule.serializeServerData(serverData),
|
|
3487
|
+
locale
|
|
3377
3488
|
});
|
|
3378
3489
|
if (renderMode === "prerender" || overrideMode === "prerender") isrSet(url, finalHtml);
|
|
3379
3490
|
return c.html(finalHtml);
|
|
@@ -3431,6 +3542,8 @@ function finesoftFrontViteConfig(options = {}) {
|
|
|
3431
3542
|
templateHtml,
|
|
3432
3543
|
renderModes: options.renderModes,
|
|
3433
3544
|
proxies: options.proxies,
|
|
3545
|
+
locales: options.locales,
|
|
3546
|
+
defaultLocale: options.defaultLocale,
|
|
3434
3547
|
resolvedResolve,
|
|
3435
3548
|
resolvedCss,
|
|
3436
3549
|
vite,
|
|
@@ -3456,6 +3569,6 @@ function finesoftFrontViteConfig(options = {}) {
|
|
|
3456
3569
|
};
|
|
3457
3570
|
}
|
|
3458
3571
|
//#endregion
|
|
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 };
|
|
3572
|
+
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, resolveLocaleFromUrl, resolvePluralKey, resolveRoot, rewrite, runAfterLoadGuards, runBeforeLoadGuards, serializeServerData, setHtmlLocaleAttributes, shouldLog, ssrRender, stableStringify, startBrowserApp, startServer, staticAdapter, tryScroll, vercelAdapter };
|
|
3460
3573
|
|
|
3461
3574
|
//# sourceMappingURL=index.mjs.map
|