@fluixi/core 1.0.0-alpha.100
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/LICENSE +21 -0
- package/README.md +334 -0
- package/dist/cdn/core.global.js +2 -0
- package/dist/cli/generate.cjs +5639 -0
- package/dist/cli/generate.d.ts +7 -0
- package/dist/cli/generate.d.ts.map +1 -0
- package/dist/cli/generate.js +2580 -0
- package/dist/cli/generate.mjs +5644 -0
- package/dist/cli/index.cjs +5639 -0
- package/dist/cli/index.d.ts +7 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +2580 -0
- package/dist/cli/index.mjs +5644 -0
- package/dist/cli/run.cjs +64 -0
- package/dist/cli/run.d.ts +3 -0
- package/dist/cli/run.d.ts.map +1 -0
- package/dist/cli/run.js +62 -0
- package/dist/cli/run.mjs +40 -0
- package/dist/fluixi.d.ts +5 -0
- package/dist/src/cdn/chunk-RKBFEFZP.mjs +987 -0
- package/dist/src/cdn/core.cjs +1592 -0
- package/dist/src/cdn/core.d.ts +16 -0
- package/dist/src/cdn/core.d.ts.map +1 -0
- package/dist/src/cdn/core.js +15 -0
- package/dist/src/cdn/core.mjs +714 -0
- package/dist/src/cdn/router-V2THK27H.mjs +74 -0
- package/dist/src/index.cjs +1711 -0
- package/dist/src/index.d.ts +4 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +11 -0
- package/dist/src/index.mjs +1833 -0
- package/dist/src/jsx-dev-runtime.cjs +26 -0
- package/dist/src/jsx-dev-runtime.d.ts +2 -0
- package/dist/src/jsx-dev-runtime.d.ts.map +1 -0
- package/dist/src/jsx-dev-runtime.js +1 -0
- package/dist/src/jsx-dev-runtime.mjs +24 -0
- package/dist/src/jsx-runtime.cjs +26 -0
- package/dist/src/jsx-runtime.d.ts +7 -0
- package/dist/src/jsx-runtime.d.ts.map +1 -0
- package/dist/src/jsx-runtime.js +8 -0
- package/dist/src/jsx-runtime.mjs +24 -0
- package/dist/src/lib/client/chunk-FDOPDB6Z.mjs +53 -0
- package/dist/src/lib/client/index.cjs +1210 -0
- package/dist/src/lib/client/index.d.ts +51 -0
- package/dist/src/lib/client/index.d.ts.map +1 -0
- package/dist/src/lib/client/index.js +174 -0
- package/dist/src/lib/client/index.mjs +293 -0
- package/dist/src/lib/client/router-4PTBPZLQ.mjs +891 -0
- package/dist/src/lib/context/context.cjs +81 -0
- package/dist/src/lib/context/context.d.ts +73 -0
- package/dist/src/lib/context/context.d.ts.map +1 -0
- package/dist/src/lib/context/context.js +93 -0
- package/dist/src/lib/context/context.mjs +64 -0
- package/dist/src/lib/context/index.cjs +83 -0
- package/dist/src/lib/context/index.d.ts +3 -0
- package/dist/src/lib/context/index.d.ts.map +1 -0
- package/dist/src/lib/context/index.js +1 -0
- package/dist/src/lib/context/index.mjs +64 -0
- package/dist/src/lib/control-flow.cjs +35 -0
- package/dist/src/lib/control-flow.d.ts +4 -0
- package/dist/src/lib/control-flow.d.ts.map +1 -0
- package/dist/src/lib/control-flow.js +1 -0
- package/dist/src/lib/control-flow.mjs +25 -0
- package/dist/src/lib/core.cjs +1710 -0
- package/dist/src/lib/core.d.ts +91 -0
- package/dist/src/lib/core.d.ts.map +1 -0
- package/dist/src/lib/core.js +158 -0
- package/dist/src/lib/core.mjs +1833 -0
- package/dist/src/lib/env/index.cjs +44 -0
- package/dist/src/lib/env/index.d.ts +26 -0
- package/dist/src/lib/env/index.d.ts.map +1 -0
- package/dist/src/lib/env/index.js +31 -0
- package/dist/src/lib/env/index.mjs +23 -0
- package/dist/src/lib/i18n/index.cjs +123 -0
- package/dist/src/lib/i18n/index.d.ts +110 -0
- package/dist/src/lib/i18n/index.d.ts.map +1 -0
- package/dist/src/lib/i18n/index.js +164 -0
- package/dist/src/lib/i18n/index.mjs +103 -0
- package/dist/src/lib/index.cjs +598 -0
- package/dist/src/lib/index.d.ts +81 -0
- package/dist/src/lib/index.d.ts.map +1 -0
- package/dist/src/lib/index.js +130 -0
- package/dist/src/lib/index.mjs +688 -0
- package/dist/src/lib/isomorphic.cjs +90 -0
- package/dist/src/lib/isomorphic.d.ts +114 -0
- package/dist/src/lib/isomorphic.d.ts.map +1 -0
- package/dist/src/lib/isomorphic.js +132 -0
- package/dist/src/lib/isomorphic.mjs +69 -0
- package/dist/src/lib/jsx-runtime/index.cjs +21 -0
- package/dist/src/lib/jsx-runtime/index.d.ts +13 -0
- package/dist/src/lib/jsx-runtime/index.d.ts.map +1 -0
- package/dist/src/lib/jsx-runtime/index.js +13 -0
- package/dist/src/lib/jsx-runtime/index.mjs +4 -0
- package/dist/src/lib/jsx-runtime/jsx-dev-runtime.cjs +21 -0
- package/dist/src/lib/jsx-runtime/jsx-dev-runtime.d.ts +2 -0
- package/dist/src/lib/jsx-runtime/jsx-dev-runtime.d.ts.map +1 -0
- package/dist/src/lib/jsx-runtime/jsx-dev-runtime.js +1 -0
- package/dist/src/lib/jsx-runtime/jsx-dev-runtime.mjs +4 -0
- package/dist/src/lib/plugins/fluixi-routes-plugin.cjs +302 -0
- package/dist/src/lib/plugins/fluixi-routes-plugin.d.ts +35 -0
- package/dist/src/lib/plugins/fluixi-routes-plugin.d.ts.map +1 -0
- package/dist/src/lib/plugins/fluixi-routes-plugin.js +118 -0
- package/dist/src/lib/plugins/fluixi-routes-plugin.mjs +271 -0
- package/dist/src/lib/plugins/index.cjs +311 -0
- package/dist/src/lib/plugins/index.d.ts +6 -0
- package/dist/src/lib/plugins/index.d.ts.map +1 -0
- package/dist/src/lib/plugins/index.js +7 -0
- package/dist/src/lib/plugins/index.mjs +279 -0
- package/dist/src/lib/plugins/route-codegen.cjs +73 -0
- package/dist/src/lib/plugins/route-codegen.d.ts +12 -0
- package/dist/src/lib/plugins/route-codegen.d.ts.map +1 -0
- package/dist/src/lib/plugins/route-codegen.js +80 -0
- package/dist/src/lib/plugins/route-codegen.mjs +52 -0
- package/dist/src/lib/plugins/route-scanner.cjs +189 -0
- package/dist/src/lib/plugins/route-scanner.d.ts +38 -0
- package/dist/src/lib/plugins/route-scanner.d.ts.map +1 -0
- package/dist/src/lib/plugins/route-scanner.js +215 -0
- package/dist/src/lib/plugins/route-scanner.mjs +158 -0
- package/dist/src/lib/plugins/vite.cjs +27 -0
- package/dist/src/lib/plugins/vite.d.ts +3 -0
- package/dist/src/lib/plugins/vite.d.ts.map +1 -0
- package/dist/src/lib/plugins/vite.js +1 -0
- package/dist/src/lib/plugins/vite.mjs +6 -0
- package/dist/src/lib/render/await.cjs +38 -0
- package/dist/src/lib/render/await.d.ts +26 -0
- package/dist/src/lib/render/await.d.ts.map +1 -0
- package/dist/src/lib/render/await.js +45 -0
- package/dist/src/lib/render/await.mjs +17 -0
- package/dist/src/lib/render/component.cjs +221 -0
- package/dist/src/lib/render/component.d.ts +296 -0
- package/dist/src/lib/render/component.d.ts.map +1 -0
- package/dist/src/lib/render/component.js +514 -0
- package/dist/src/lib/render/component.mjs +214 -0
- package/dist/src/lib/render/deferred.cjs +175 -0
- package/dist/src/lib/render/deferred.d.ts +36 -0
- package/dist/src/lib/render/deferred.d.ts.map +1 -0
- package/dist/src/lib/render/deferred.js +141 -0
- package/dist/src/lib/render/deferred.mjs +158 -0
- package/dist/src/lib/render/index.cjs +573 -0
- package/dist/src/lib/render/index.d.ts +98 -0
- package/dist/src/lib/render/index.d.ts.map +1 -0
- package/dist/src/lib/render/index.js +180 -0
- package/dist/src/lib/render/index.mjs +590 -0
- package/dist/src/lib/render/lazy.cjs +67 -0
- package/dist/src/lib/render/lazy.d.ts +49 -0
- package/dist/src/lib/render/lazy.d.ts.map +1 -0
- package/dist/src/lib/render/lazy.js +105 -0
- package/dist/src/lib/render/lazy.mjs +50 -0
- package/dist/src/lib/render/suspense.cjs +141 -0
- package/dist/src/lib/render/suspense.d.ts +65 -0
- package/dist/src/lib/render/suspense.d.ts.map +1 -0
- package/dist/src/lib/render/suspense.js +196 -0
- package/dist/src/lib/render/suspense.mjs +136 -0
- package/dist/src/lib/render/versions.cjs +35 -0
- package/dist/src/lib/render/versions.d.ts +2 -0
- package/dist/src/lib/render/versions.d.ts.map +1 -0
- package/dist/src/lib/render/versions.js +21 -0
- package/dist/src/lib/render/versions.mjs +14 -0
- package/dist/src/lib/router/data.cjs +228 -0
- package/dist/src/lib/router/data.d.ts +142 -0
- package/dist/src/lib/router/data.d.ts.map +1 -0
- package/dist/src/lib/router/data.js +258 -0
- package/dist/src/lib/router/data.mjs +214 -0
- package/dist/src/lib/router/index.cjs +921 -0
- package/dist/src/lib/router/index.d.ts +274 -0
- package/dist/src/lib/router/index.d.ts.map +1 -0
- package/dist/src/lib/router/index.js +904 -0
- package/dist/src/lib/router/index.mjs +938 -0
- package/dist/src/lib/router/scroll.cjs +79 -0
- package/dist/src/lib/router/scroll.d.ts +19 -0
- package/dist/src/lib/router/scroll.d.ts.map +1 -0
- package/dist/src/lib/router/scroll.js +68 -0
- package/dist/src/lib/router/scroll.mjs +58 -0
- package/dist/src/lib/router/transition.cjs +75 -0
- package/dist/src/lib/router/transition.d.ts +81 -0
- package/dist/src/lib/router/transition.d.ts.map +1 -0
- package/dist/src/lib/router/transition.js +136 -0
- package/dist/src/lib/router/transition.mjs +54 -0
- package/dist/src/lib/server/hydration.cjs +169 -0
- package/dist/src/lib/server/hydration.d.ts +198 -0
- package/dist/src/lib/server/hydration.d.ts.map +1 -0
- package/dist/src/lib/server/hydration.js +345 -0
- package/dist/src/lib/server/hydration.mjs +148 -0
- package/dist/src/lib/server/index.cjs +406 -0
- package/dist/src/lib/server/index.d.ts +115 -0
- package/dist/src/lib/server/index.d.ts.map +1 -0
- package/dist/src/lib/server/index.js +164 -0
- package/dist/src/lib/server/index.mjs +384 -0
- package/dist/src/lib/server/reactive.cjs +197 -0
- package/dist/src/lib/server/reactive.d.ts +239 -0
- package/dist/src/lib/server/reactive.d.ts.map +1 -0
- package/dist/src/lib/server/reactive.js +413 -0
- package/dist/src/lib/server/reactive.mjs +176 -0
- package/dist/src/lib/storage/cache.cjs +51 -0
- package/dist/src/lib/storage/cache.d.ts +18 -0
- package/dist/src/lib/storage/cache.d.ts.map +1 -0
- package/dist/src/lib/storage/cache.js +42 -0
- package/dist/src/lib/storage/cache.mjs +30 -0
- package/dist/src/lib/storage/index.cjs +246 -0
- package/dist/src/lib/storage/index.d.ts +83 -0
- package/dist/src/lib/storage/index.d.ts.map +1 -0
- package/dist/src/lib/storage/index.js +179 -0
- package/dist/src/lib/storage/index.mjs +231 -0
- package/dist/src/lib/storage/persisted.cjs +91 -0
- package/dist/src/lib/storage/persisted.d.ts +53 -0
- package/dist/src/lib/storage/persisted.d.ts.map +1 -0
- package/dist/src/lib/storage/persisted.js +75 -0
- package/dist/src/lib/storage/persisted.mjs +70 -0
- package/dist/src/lib/theme/color-scheme.cjs +281 -0
- package/dist/src/lib/theme/color-scheme.d.ts +61 -0
- package/dist/src/lib/theme/color-scheme.d.ts.map +1 -0
- package/dist/src/lib/theme/color-scheme.js +120 -0
- package/dist/src/lib/theme/color-scheme.mjs +265 -0
- package/dist/src/lib/theme/index.cjs +267 -0
- package/dist/src/lib/theme/index.d.ts +78 -0
- package/dist/src/lib/theme/index.d.ts.map +1 -0
- package/dist/src/lib/theme/index.js +135 -0
- package/dist/src/lib/theme/index.mjs +251 -0
- package/dist/src/lib/theme/provider.cjs +346 -0
- package/dist/src/lib/theme/provider.d.ts +34 -0
- package/dist/src/lib/theme/provider.d.ts.map +1 -0
- package/dist/src/lib/theme/provider.js +60 -0
- package/dist/src/lib/theme/provider.mjs +332 -0
- package/dist/src/lib/utils/index.cjs +209 -0
- package/dist/src/lib/utils/index.d.ts +286 -0
- package/dist/src/lib/utils/index.d.ts.map +1 -0
- package/dist/src/lib/utils/index.js +434 -0
- package/dist/src/lib/utils/index.mjs +188 -0
- package/dist/src/routes.cjs +37 -0
- package/dist/src/routes.d.ts +22 -0
- package/dist/src/routes.d.ts.map +1 -0
- package/dist/src/routes.js +20 -0
- package/dist/src/routes.mjs +17 -0
- package/dist/src/server.cjs +405 -0
- package/dist/src/server.d.ts +63 -0
- package/dist/src/server.d.ts.map +1 -0
- package/dist/src/server.js +62 -0
- package/dist/src/server.mjs +382 -0
- package/dist/src/utils.cjs +21 -0
- package/dist/src/utils.d.ts +10 -0
- package/dist/src/utils.d.ts.map +1 -0
- package/dist/src/utils.js +9 -0
- package/dist/src/utils.mjs +4 -0
- package/dist/src/version.generated.cjs +26 -0
- package/dist/src/version.generated.d.ts +3 -0
- package/dist/src/version.generated.d.ts.map +1 -0
- package/dist/src/version.generated.js +3 -0
- package/dist/src/version.generated.mjs +5 -0
- package/dist/tsconfig.lib.tsbuildinfo +1 -0
- package/package.json +164 -0
- package/src/index.ts +19 -0
- package/types/fluixi.d.ts +5 -0
- package/types/vinxi.d.ts +50 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/lib/env/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
clientOnly: () => clientOnly,
|
|
24
|
+
env: () => env,
|
|
25
|
+
isClient: () => isClient,
|
|
26
|
+
isServer: () => import_dom.isServer
|
|
27
|
+
});
|
|
28
|
+
module.exports = __toCommonJS(index_exports);
|
|
29
|
+
var import_dom = require("@fluixi/dom");
|
|
30
|
+
var import_dom2 = require("@fluixi/dom");
|
|
31
|
+
function isClient() {
|
|
32
|
+
return !(0, import_dom2.isServer)();
|
|
33
|
+
}
|
|
34
|
+
var env = {
|
|
35
|
+
get window() {
|
|
36
|
+
return typeof window !== "undefined" ? window : void 0;
|
|
37
|
+
},
|
|
38
|
+
get document() {
|
|
39
|
+
return typeof document !== "undefined" ? document : void 0;
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
function clientOnly(fn, fallback) {
|
|
43
|
+
return isClient() ? fn() : fallback;
|
|
44
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Universal environment utilities, the same imports work on the server and the
|
|
3
|
+
* client, so app code never reaches for raw globals (`window`, `document`) that don't
|
|
4
|
+
* exist under SSR and crash at module-eval time. Storage is `@fluixi/core/storage`.
|
|
5
|
+
*/
|
|
6
|
+
export { isServer } from '@fluixi/dom';
|
|
7
|
+
/** True in a browser (or jsdom) environment. */
|
|
8
|
+
export declare function isClient(): boolean;
|
|
9
|
+
/**
|
|
10
|
+
* Lazy, typed accessors for the browser globals. They return the real object on the
|
|
11
|
+
* client and `undefined` on the server, so `env.window?.matchMedia(...)` is safe and
|
|
12
|
+
* typed everywhere. Deliberately not faked on the server: a stub `location`/`innerWidth`
|
|
13
|
+
* would return wrong values and cause hydration mismatches: guard with `?.` instead.
|
|
14
|
+
*/
|
|
15
|
+
export declare const env: {
|
|
16
|
+
readonly window: (Window & typeof globalThis) | undefined;
|
|
17
|
+
readonly document: Document | undefined;
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* Run `fn` only on the client; on the server return `fallback` (or `undefined`).
|
|
21
|
+
* For values that genuinely don't exist server-side, e.g.
|
|
22
|
+
* `clientOnly(() => window.innerWidth, 0)`.
|
|
23
|
+
*/
|
|
24
|
+
export declare function clientOnly<T>(fn: () => T): T | undefined;
|
|
25
|
+
export declare function clientOnly<T>(fn: () => T, fallback: T): T;
|
|
26
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/lib/env/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAKH,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAGvC,gDAAgD;AAChD,wBAAgB,QAAQ,IAAI,OAAO,CAElC;AAED;;;;;GAKG;AACH,eAAO,MAAM,GAAG;qBACA,CAAC,MAAM,GAAG,OAAO,UAAU,CAAC,GAAG,SAAS;uBAGtC,QAAQ,GAAG,SAAS;CAGrC,CAAC;AAEF;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,CAAC,GAAG,CAAC,GAAG,SAAS,CAAC;AAC1D,wBAAgB,UAAU,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,CAAC,EAAE,QAAQ,EAAE,CAAC,GAAG,CAAC,CAAC"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Universal environment utilities, the same imports work on the server and the
|
|
3
|
+
* client, so app code never reaches for raw globals (`window`, `document`) that don't
|
|
4
|
+
* exist under SSR and crash at module-eval time. Storage is `@fluixi/core/storage`.
|
|
5
|
+
*/
|
|
6
|
+
// One answer for the whole framework, from the renderer that decides it. The constant
|
|
7
|
+
// this used to be was read once at module eval, so it could not see renderToString turn
|
|
8
|
+
// server mode on and reported false for the whole of a server render.
|
|
9
|
+
export { isServer } from '@fluixi/dom';
|
|
10
|
+
import { isServer as onServer } from '@fluixi/dom';
|
|
11
|
+
/** True in a browser (or jsdom) environment. */
|
|
12
|
+
export function isClient() {
|
|
13
|
+
return !onServer();
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Lazy, typed accessors for the browser globals. They return the real object on the
|
|
17
|
+
* client and `undefined` on the server, so `env.window?.matchMedia(...)` is safe and
|
|
18
|
+
* typed everywhere. Deliberately not faked on the server: a stub `location`/`innerWidth`
|
|
19
|
+
* would return wrong values and cause hydration mismatches: guard with `?.` instead.
|
|
20
|
+
*/
|
|
21
|
+
export const env = {
|
|
22
|
+
get window() {
|
|
23
|
+
return typeof window !== 'undefined' ? window : undefined;
|
|
24
|
+
},
|
|
25
|
+
get document() {
|
|
26
|
+
return typeof document !== 'undefined' ? document : undefined;
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
export function clientOnly(fn, fallback) {
|
|
30
|
+
return isClient() ? fn() : fallback;
|
|
31
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// src/lib/env/index.ts
|
|
2
|
+
import { isServer } from "@fluixi/dom";
|
|
3
|
+
import { isServer as onServer } from "@fluixi/dom";
|
|
4
|
+
function isClient() {
|
|
5
|
+
return !onServer();
|
|
6
|
+
}
|
|
7
|
+
var env = {
|
|
8
|
+
get window() {
|
|
9
|
+
return typeof window !== "undefined" ? window : void 0;
|
|
10
|
+
},
|
|
11
|
+
get document() {
|
|
12
|
+
return typeof document !== "undefined" ? document : void 0;
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
function clientOnly(fn, fallback) {
|
|
16
|
+
return isClient() ? fn() : fallback;
|
|
17
|
+
}
|
|
18
|
+
export {
|
|
19
|
+
clientOnly,
|
|
20
|
+
env,
|
|
21
|
+
isClient,
|
|
22
|
+
isServer
|
|
23
|
+
};
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/*! @fluixi/core v1.0.0-alpha.100 | (c) 2026 Ibrahima Touré and Fluixi contributors | MIT */
|
|
2
|
+
"use strict";
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
var __copyProps = (to, from, except, desc) => {
|
|
12
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
13
|
+
for (let key of __getOwnPropNames(from))
|
|
14
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
15
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
16
|
+
}
|
|
17
|
+
return to;
|
|
18
|
+
};
|
|
19
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
20
|
+
|
|
21
|
+
// src/lib/i18n/index.ts
|
|
22
|
+
var index_exports = {};
|
|
23
|
+
__export(index_exports, {
|
|
24
|
+
createI18n: () => createI18n,
|
|
25
|
+
detectLocale: () => detectLocale
|
|
26
|
+
});
|
|
27
|
+
module.exports = __toCommonJS(index_exports);
|
|
28
|
+
var import_signal = require("@fluixi/reactive/signal");
|
|
29
|
+
var import_dom = require("@fluixi/dom");
|
|
30
|
+
var RELATIVE_UNITS = [
|
|
31
|
+
["year", 365 * 24 * 3600],
|
|
32
|
+
["month", 30 * 24 * 3600],
|
|
33
|
+
["week", 7 * 24 * 3600],
|
|
34
|
+
["day", 24 * 3600],
|
|
35
|
+
["hour", 3600],
|
|
36
|
+
["minute", 60],
|
|
37
|
+
["second", 1]
|
|
38
|
+
];
|
|
39
|
+
var SEED_KEY = "__i18n:locale";
|
|
40
|
+
var IS_SERVER = typeof document === "undefined";
|
|
41
|
+
function readSeed() {
|
|
42
|
+
if (IS_SERVER) return void 0;
|
|
43
|
+
const data = (0, import_dom.getServerData)();
|
|
44
|
+
return data ? data[SEED_KEY] : void 0;
|
|
45
|
+
}
|
|
46
|
+
function cookiePersist(locale) {
|
|
47
|
+
if (typeof document === "undefined") return;
|
|
48
|
+
try {
|
|
49
|
+
document.cookie = `locale=${locale};path=/;max-age=31536000;samesite=lax`;
|
|
50
|
+
} catch {
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
function createI18n(config) {
|
|
54
|
+
const persist = config.persist ?? cookiePersist;
|
|
55
|
+
const [clientLocale, setClientLocale] = (0, import_signal.createSignal)(readSeed() ?? config.locale);
|
|
56
|
+
const serverCtx = () => IS_SERVER ? (0, import_dom.getRequestContext)() : void 0;
|
|
57
|
+
function locale() {
|
|
58
|
+
const ctx = serverCtx();
|
|
59
|
+
if (ctx) return ctx.__i18nLocale ?? config.locale;
|
|
60
|
+
return clientLocale();
|
|
61
|
+
}
|
|
62
|
+
function setLocale(next) {
|
|
63
|
+
const ctx = serverCtx();
|
|
64
|
+
if (ctx) {
|
|
65
|
+
ctx.__i18nLocale = next;
|
|
66
|
+
ctx.data?.set?.(SEED_KEY, next);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
setClientLocale(() => next);
|
|
70
|
+
persist(next);
|
|
71
|
+
}
|
|
72
|
+
function resolve(loc, key) {
|
|
73
|
+
const bundle = config.messages[loc];
|
|
74
|
+
if (bundle == null) return void 0;
|
|
75
|
+
const flat = bundle[key];
|
|
76
|
+
if (flat !== void 0) return flat;
|
|
77
|
+
let node = bundle;
|
|
78
|
+
for (const part of key.split(".")) {
|
|
79
|
+
if (node == null || typeof node !== "object") return void 0;
|
|
80
|
+
node = node[part];
|
|
81
|
+
}
|
|
82
|
+
return node;
|
|
83
|
+
}
|
|
84
|
+
function interpolate(str, params) {
|
|
85
|
+
if (!params) return str;
|
|
86
|
+
return str.replace(/\{(\w+)\}/g, (_m, k) => k in params ? String(params[k]) : `{${k}}`);
|
|
87
|
+
}
|
|
88
|
+
function t(key, params) {
|
|
89
|
+
const loc = locale();
|
|
90
|
+
let msg = resolve(loc, key);
|
|
91
|
+
if (msg === void 0 && loc !== config.locale) msg = resolve(config.locale, key);
|
|
92
|
+
if (msg === void 0) return key;
|
|
93
|
+
if (typeof msg !== "string") {
|
|
94
|
+
const count = Number(params?.count ?? 0);
|
|
95
|
+
const category = new Intl.PluralRules(loc).select(count);
|
|
96
|
+
const branch = msg[category] ?? msg.other ?? Object.values(msg)[0] ?? "";
|
|
97
|
+
return interpolate(branch, params);
|
|
98
|
+
}
|
|
99
|
+
return interpolate(msg, params);
|
|
100
|
+
}
|
|
101
|
+
const n = (value, options) => new Intl.NumberFormat(locale(), options).format(value);
|
|
102
|
+
const d = (value, options) => new Intl.DateTimeFormat(locale(), options).format(value);
|
|
103
|
+
const rt = (value, options = {}) => {
|
|
104
|
+
const { now = Date.now(), ...format } = options;
|
|
105
|
+
const seconds = (+value - +now) / 1e3;
|
|
106
|
+
const [unit, size] = RELATIVE_UNITS.find(([, size2]) => Math.abs(seconds) >= size2) ?? ["second", 1];
|
|
107
|
+
return new Intl.RelativeTimeFormat(locale(), { numeric: "auto", ...format }).format(Math.round(seconds / size), unit);
|
|
108
|
+
};
|
|
109
|
+
return { t, n, d, rt, locale, setLocale, locales: config.locales };
|
|
110
|
+
}
|
|
111
|
+
function detectLocale(acceptLanguage, locales) {
|
|
112
|
+
if (!acceptLanguage) return void 0;
|
|
113
|
+
const ranked = acceptLanguage.split(",").map((part) => {
|
|
114
|
+
const [tag, q] = part.trim().split(";q=");
|
|
115
|
+
return { tag: tag.toLowerCase(), q: q ? parseFloat(q) : 1 };
|
|
116
|
+
}).sort((a, b) => b.q - a.q);
|
|
117
|
+
for (const { tag } of ranked) {
|
|
118
|
+
const base = tag.split("-")[0];
|
|
119
|
+
const hit = locales.find((l) => l.toLowerCase() === tag || l.toLowerCase() === base);
|
|
120
|
+
if (hit) return hit;
|
|
121
|
+
}
|
|
122
|
+
return void 0;
|
|
123
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fluixi/core/i18n: built-in, reactive, SSR-correct internationalization.
|
|
3
|
+
*
|
|
4
|
+
* Three things make this distinct:
|
|
5
|
+
*
|
|
6
|
+
* 1. **SSR request-scoped locale.** The active locale is per-request on the server (kept in the
|
|
7
|
+
* RequestContext) and a reactive module signal on the client. Two concurrent SSR requests in
|
|
8
|
+
* different languages never bleed, and the server-resolved locale is seeded to the client
|
|
9
|
+
* (no flash, no hydration mismatch).
|
|
10
|
+
*
|
|
11
|
+
* 2. **No reactive-owner-context dependency.** `t()` reads the locale from the request context /
|
|
12
|
+
* module signal, not `useContext`, so it resolves in lazy routes, server functions, loaders,
|
|
13
|
+
* anywhere. (This is precisely why it sidesteps the classic "useLocale() is undefined under
|
|
14
|
+
* SSR on a lazy route" failure: there's no provider owner-chain to lose.)
|
|
15
|
+
*
|
|
16
|
+
* 3. **Platform-native formatting.** Pluralization via `Intl.PluralRules`, numbers/dates via
|
|
17
|
+
* `Intl.NumberFormat`/`Intl.DateTimeFormat`: correct CLDR rules, zero dependencies.
|
|
18
|
+
*
|
|
19
|
+
* const i18n = createI18n({ locale: 'en', locales: ['en', 'fr'] as const, messages: { en, fr } });
|
|
20
|
+
* export const { t, n, d, locale, setLocale } = i18n;
|
|
21
|
+
*
|
|
22
|
+
* t('welcome', { name: 'Ada' }) // "Welcome, Ada"
|
|
23
|
+
* t('cart.items', { count: 3 }) // plural branch via Intl.PluralRules
|
|
24
|
+
* n(1234.5); d(new Date()); // locale-aware number/date
|
|
25
|
+
* setLocale('fr'); // reactive, every t() re-evaluates
|
|
26
|
+
*/
|
|
27
|
+
import { type Accessor } from '@fluixi/reactive/signal';
|
|
28
|
+
/** A leaf message: a plain string, or a plural map keyed by CLDR category (`one`/`other`/...). */
|
|
29
|
+
export type MessageValue = string | {
|
|
30
|
+
readonly [pluralCategory: string]: string;
|
|
31
|
+
};
|
|
32
|
+
/** A (possibly nested) message tree. */
|
|
33
|
+
export interface Messages {
|
|
34
|
+
readonly [key: string]: MessageValue | Messages;
|
|
35
|
+
}
|
|
36
|
+
/** CLDR plural categories. An object keyed *only* by these is a plural leaf, not a subtree. */
|
|
37
|
+
type PluralCategory = 'zero' | 'one' | 'two' | 'few' | 'many' | 'other';
|
|
38
|
+
/**
|
|
39
|
+
* The union of every valid translation key in `M`: `t()` autocompletes these and flags typos.
|
|
40
|
+
* Flat keys (`'welcome'`, `'nav.dashboard'`), nested trees (`'cart.items'`, `'nested.deep.key'`),
|
|
41
|
+
* and plural maps (`{ one, other }` -> a leaf) all work. Only *leaf* paths are emitted, never the
|
|
42
|
+
* intermediate branch (`'nav'`, `'cart'`). Falls open to `string` when `M` is just `Messages`.
|
|
43
|
+
*
|
|
44
|
+
* A subtree and a plural map are both `Record<string, string>` structurally, so the plural leaf is
|
|
45
|
+
* detected by its keys being CLDR categories, not by "all children are strings" (which wrongly
|
|
46
|
+
* truncated `nav: { dashboard, settings }` to `'nav'`).
|
|
47
|
+
*/
|
|
48
|
+
export type MessageKey<M> = M extends object ? {
|
|
49
|
+
[K in keyof M & string]: M[K] extends string ? K : [keyof M[K]] extends [PluralCategory] ? K : M[K] extends object ? `${K}.${MessageKey<M[K]>}` : K;
|
|
50
|
+
}[keyof M & string] : string;
|
|
51
|
+
export interface I18nConfig<L extends string, M extends Record<L, Messages> = Record<L, Messages>> {
|
|
52
|
+
/** Default / fallback locale. */
|
|
53
|
+
locale: L;
|
|
54
|
+
/** Supported locales. */
|
|
55
|
+
locales: readonly L[];
|
|
56
|
+
/** Messages per locale (nested; a leaf is a string or a plural map). Keys are inferred for `t`. */
|
|
57
|
+
messages: M;
|
|
58
|
+
/** Persist the chosen locale on the client. Default: a year-long `locale` cookie. */
|
|
59
|
+
persist?: (locale: L) => void;
|
|
60
|
+
}
|
|
61
|
+
export interface RelativeTimeOptions extends Intl.RelativeTimeFormatOptions {
|
|
62
|
+
/** The moment `value` is relative to. Defaults to the current time. */
|
|
63
|
+
now?: Date | number;
|
|
64
|
+
}
|
|
65
|
+
export interface I18n<L extends string, K extends string = string> {
|
|
66
|
+
/** Translate a key (autocompleted from your messages) with optional params; `count` pluralizes. */
|
|
67
|
+
t: (key: K | (string & {}), params?: Record<string, unknown>) => string;
|
|
68
|
+
/** Format a number for the active locale (`Intl.NumberFormat`). */
|
|
69
|
+
n: (value: number, options?: Intl.NumberFormatOptions) => string;
|
|
70
|
+
/** Format a date for the active locale (`Intl.DateTimeFormat`). */
|
|
71
|
+
d: (value: Date | number, options?: Intl.DateTimeFormatOptions) => string;
|
|
72
|
+
/**
|
|
73
|
+
* A date relative to now, for the active locale (`Intl.RelativeTimeFormat`): "yesterday",
|
|
74
|
+
* "3 days ago", "in 2 hours". The unit is the largest that fits. Pass `now` when the server
|
|
75
|
+
* and the client must agree, such as a time the server sent with the data.
|
|
76
|
+
*/
|
|
77
|
+
rt: (value: Date | number, options?: RelativeTimeOptions) => string;
|
|
78
|
+
/** The active locale: reactive on the client, per-request on the server. */
|
|
79
|
+
locale: Accessor<L>;
|
|
80
|
+
/** Switch the active locale (client: reactive + persisted; server: this request only). */
|
|
81
|
+
setLocale: (locale: L) => void;
|
|
82
|
+
/** Supported locales. */
|
|
83
|
+
locales: readonly L[];
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Translations, with the message keys inferred from the messages themselves.
|
|
87
|
+
*
|
|
88
|
+
* `t()` accepts only leaf paths that exist in the catalogue, so a typo is a type error
|
|
89
|
+
* rather than a string echoed back at a visitor. A missing key at runtime returns the key,
|
|
90
|
+
* which is visible in the page and traceable.
|
|
91
|
+
*
|
|
92
|
+
* The locale is per request on the server and reactive on the client, seeded from what the
|
|
93
|
+
* server resolved so hydration cannot disagree about the language. Persistence is a cookie
|
|
94
|
+
* by default, which is the only store a server render can read.
|
|
95
|
+
*
|
|
96
|
+
* @example
|
|
97
|
+
* ```ts
|
|
98
|
+
* const i18n = createI18n({
|
|
99
|
+
* locale: 'en',
|
|
100
|
+
* messages: { en: { hi: 'Hello' }, fr: { hi: 'Bonjour' } },
|
|
101
|
+
* });
|
|
102
|
+
* i18n.t('hi'); // 'Hello'
|
|
103
|
+
* i18n.setLocale('fr');
|
|
104
|
+
* ```
|
|
105
|
+
*/
|
|
106
|
+
export declare function createI18n<L extends string, const M extends Record<L, Messages>>(config: I18nConfig<L, M>): I18n<L, MessageKey<M[keyof M]>>;
|
|
107
|
+
/** Pick the best supported locale from an `Accept-Language` header, else `undefined`. */
|
|
108
|
+
export declare function detectLocale<L extends string>(acceptLanguage: string | null | undefined, locales: readonly L[]): L | undefined;
|
|
109
|
+
export {};
|
|
110
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/lib/i18n/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fluixi/core/i18n: built-in, reactive, SSR-correct internationalization.
|
|
3
|
+
*
|
|
4
|
+
* Three things make this distinct:
|
|
5
|
+
*
|
|
6
|
+
* 1. **SSR request-scoped locale.** The active locale is per-request on the server (kept in the
|
|
7
|
+
* RequestContext) and a reactive module signal on the client. Two concurrent SSR requests in
|
|
8
|
+
* different languages never bleed, and the server-resolved locale is seeded to the client
|
|
9
|
+
* (no flash, no hydration mismatch).
|
|
10
|
+
*
|
|
11
|
+
* 2. **No reactive-owner-context dependency.** `t()` reads the locale from the request context /
|
|
12
|
+
* module signal, not `useContext`, so it resolves in lazy routes, server functions, loaders,
|
|
13
|
+
* anywhere. (This is precisely why it sidesteps the classic "useLocale() is undefined under
|
|
14
|
+
* SSR on a lazy route" failure: there's no provider owner-chain to lose.)
|
|
15
|
+
*
|
|
16
|
+
* 3. **Platform-native formatting.** Pluralization via `Intl.PluralRules`, numbers/dates via
|
|
17
|
+
* `Intl.NumberFormat`/`Intl.DateTimeFormat`: correct CLDR rules, zero dependencies.
|
|
18
|
+
*
|
|
19
|
+
* const i18n = createI18n({ locale: 'en', locales: ['en', 'fr'] as const, messages: { en, fr } });
|
|
20
|
+
* export const { t, n, d, locale, setLocale } = i18n;
|
|
21
|
+
*
|
|
22
|
+
* t('welcome', { name: 'Ada' }) // "Welcome, Ada"
|
|
23
|
+
* t('cart.items', { count: 3 }) // plural branch via Intl.PluralRules
|
|
24
|
+
* n(1234.5); d(new Date()); // locale-aware number/date
|
|
25
|
+
* setLocale('fr'); // reactive, every t() re-evaluates
|
|
26
|
+
*/
|
|
27
|
+
import { createSignal } from '@fluixi/reactive/signal';
|
|
28
|
+
import { getRequestContext, getServerData } from '@fluixi/dom';
|
|
29
|
+
/** Largest first: the first unit the distance reaches is the one shown. */
|
|
30
|
+
const RELATIVE_UNITS = [
|
|
31
|
+
['year', 365 * 24 * 3600],
|
|
32
|
+
['month', 30 * 24 * 3600],
|
|
33
|
+
['week', 7 * 24 * 3600],
|
|
34
|
+
['day', 24 * 3600],
|
|
35
|
+
['hour', 3600],
|
|
36
|
+
['minute', 60],
|
|
37
|
+
['second', 1],
|
|
38
|
+
];
|
|
39
|
+
const SEED_KEY = '__i18n:locale';
|
|
40
|
+
const IS_SERVER = typeof document === 'undefined';
|
|
41
|
+
function readSeed() {
|
|
42
|
+
if (IS_SERVER)
|
|
43
|
+
return undefined;
|
|
44
|
+
const data = getServerData();
|
|
45
|
+
return data ? data[SEED_KEY] : undefined;
|
|
46
|
+
}
|
|
47
|
+
function cookiePersist(locale) {
|
|
48
|
+
if (typeof document === 'undefined')
|
|
49
|
+
return;
|
|
50
|
+
try {
|
|
51
|
+
document.cookie = `locale=${locale};path=/;max-age=31536000;samesite=lax`;
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
/* private mode / disabled: best effort */
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Translations, with the message keys inferred from the messages themselves.
|
|
59
|
+
*
|
|
60
|
+
* `t()` accepts only leaf paths that exist in the catalogue, so a typo is a type error
|
|
61
|
+
* rather than a string echoed back at a visitor. A missing key at runtime returns the key,
|
|
62
|
+
* which is visible in the page and traceable.
|
|
63
|
+
*
|
|
64
|
+
* The locale is per request on the server and reactive on the client, seeded from what the
|
|
65
|
+
* server resolved so hydration cannot disagree about the language. Persistence is a cookie
|
|
66
|
+
* by default, which is the only store a server render can read.
|
|
67
|
+
*
|
|
68
|
+
* @example
|
|
69
|
+
* ```ts
|
|
70
|
+
* const i18n = createI18n({
|
|
71
|
+
* locale: 'en',
|
|
72
|
+
* messages: { en: { hi: 'Hello' }, fr: { hi: 'Bonjour' } },
|
|
73
|
+
* });
|
|
74
|
+
* i18n.t('hi'); // 'Hello'
|
|
75
|
+
* i18n.setLocale('fr');
|
|
76
|
+
* ```
|
|
77
|
+
*/
|
|
78
|
+
export function createI18n(config) {
|
|
79
|
+
const persist = config.persist ?? cookiePersist;
|
|
80
|
+
// Client: a reactive signal seeded from the server-resolved locale (so hydration matches).
|
|
81
|
+
const [clientLocale, setClientLocale] = createSignal(readSeed() ?? config.locale);
|
|
82
|
+
const serverCtx = () => IS_SERVER ? getRequestContext() : undefined;
|
|
83
|
+
function locale() {
|
|
84
|
+
const ctx = serverCtx();
|
|
85
|
+
if (ctx)
|
|
86
|
+
return ctx.__i18nLocale ?? config.locale; // server: per-request, no shared signal -> no leak
|
|
87
|
+
return clientLocale(); // client: reactive
|
|
88
|
+
}
|
|
89
|
+
function setLocale(next) {
|
|
90
|
+
const ctx = serverCtx();
|
|
91
|
+
if (ctx) {
|
|
92
|
+
ctx.__i18nLocale = next;
|
|
93
|
+
ctx.data?.set?.(SEED_KEY, next); // serialize -> client hydrates in the same language
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
setClientLocale(() => next);
|
|
97
|
+
persist(next);
|
|
98
|
+
}
|
|
99
|
+
function resolve(loc, key) {
|
|
100
|
+
const bundle = config.messages[loc];
|
|
101
|
+
if (bundle == null)
|
|
102
|
+
return undefined;
|
|
103
|
+
// Flat key first: supports dotted flat keys like `nav.dashboard` (not just nested trees).
|
|
104
|
+
const flat = bundle[key];
|
|
105
|
+
if (flat !== undefined)
|
|
106
|
+
return flat;
|
|
107
|
+
// Else walk the dot-path through a nested message tree.
|
|
108
|
+
let node = bundle;
|
|
109
|
+
for (const part of key.split('.')) {
|
|
110
|
+
if (node == null || typeof node !== 'object')
|
|
111
|
+
return undefined;
|
|
112
|
+
node = node[part];
|
|
113
|
+
}
|
|
114
|
+
return node;
|
|
115
|
+
}
|
|
116
|
+
function interpolate(str, params) {
|
|
117
|
+
if (!params)
|
|
118
|
+
return str;
|
|
119
|
+
return str.replace(/\{(\w+)\}/g, (_m, k) => (k in params ? String(params[k]) : `{${k}}`));
|
|
120
|
+
}
|
|
121
|
+
function t(key, params) {
|
|
122
|
+
const loc = locale();
|
|
123
|
+
let msg = resolve(loc, key);
|
|
124
|
+
if (msg === undefined && loc !== config.locale)
|
|
125
|
+
msg = resolve(config.locale, key); // fallback locale
|
|
126
|
+
if (msg === undefined)
|
|
127
|
+
return key; // missing -> echo the key (visible, debuggable)
|
|
128
|
+
if (typeof msg !== 'string') {
|
|
129
|
+
const count = Number(params?.count ?? 0);
|
|
130
|
+
const category = new Intl.PluralRules(loc).select(count);
|
|
131
|
+
const branch = msg[category] ?? msg.other ?? Object.values(msg)[0] ?? '';
|
|
132
|
+
return interpolate(branch, params);
|
|
133
|
+
}
|
|
134
|
+
return interpolate(msg, params);
|
|
135
|
+
}
|
|
136
|
+
const n = (value, options) => new Intl.NumberFormat(locale(), options).format(value);
|
|
137
|
+
const d = (value, options) => new Intl.DateTimeFormat(locale(), options).format(value);
|
|
138
|
+
const rt = (value, options = {}) => {
|
|
139
|
+
const { now = Date.now(), ...format } = options;
|
|
140
|
+
const seconds = (+value - +now) / 1000;
|
|
141
|
+
const [unit, size] = RELATIVE_UNITS.find(([, size]) => Math.abs(seconds) >= size) ?? ['second', 1];
|
|
142
|
+
return new Intl.RelativeTimeFormat(locale(), { numeric: 'auto', ...format }).format(Math.round(seconds / size), unit);
|
|
143
|
+
};
|
|
144
|
+
return { t, n, d, rt, locale, setLocale, locales: config.locales };
|
|
145
|
+
}
|
|
146
|
+
/** Pick the best supported locale from an `Accept-Language` header, else `undefined`. */
|
|
147
|
+
export function detectLocale(acceptLanguage, locales) {
|
|
148
|
+
if (!acceptLanguage)
|
|
149
|
+
return undefined;
|
|
150
|
+
const ranked = acceptLanguage
|
|
151
|
+
.split(',')
|
|
152
|
+
.map((part) => {
|
|
153
|
+
const [tag, q] = part.trim().split(';q=');
|
|
154
|
+
return { tag: tag.toLowerCase(), q: q ? parseFloat(q) : 1 };
|
|
155
|
+
})
|
|
156
|
+
.sort((a, b) => b.q - a.q);
|
|
157
|
+
for (const { tag } of ranked) {
|
|
158
|
+
const base = tag.split('-')[0];
|
|
159
|
+
const hit = locales.find((l) => l.toLowerCase() === tag || l.toLowerCase() === base);
|
|
160
|
+
if (hit)
|
|
161
|
+
return hit;
|
|
162
|
+
}
|
|
163
|
+
return undefined;
|
|
164
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/*! @fluixi/core v1.0.0-alpha.100 | (c) 2026 Ibrahima Touré and Fluixi contributors | MIT */
|
|
2
|
+
|
|
3
|
+
// src/lib/i18n/index.ts
|
|
4
|
+
import { createSignal } from "@fluixi/reactive/signal";
|
|
5
|
+
import { getRequestContext, getServerData } from "@fluixi/dom";
|
|
6
|
+
var RELATIVE_UNITS = [
|
|
7
|
+
["year", 365 * 24 * 3600],
|
|
8
|
+
["month", 30 * 24 * 3600],
|
|
9
|
+
["week", 7 * 24 * 3600],
|
|
10
|
+
["day", 24 * 3600],
|
|
11
|
+
["hour", 3600],
|
|
12
|
+
["minute", 60],
|
|
13
|
+
["second", 1]
|
|
14
|
+
];
|
|
15
|
+
var SEED_KEY = "__i18n:locale";
|
|
16
|
+
var IS_SERVER = typeof document === "undefined";
|
|
17
|
+
function readSeed() {
|
|
18
|
+
if (IS_SERVER) return void 0;
|
|
19
|
+
const data = getServerData();
|
|
20
|
+
return data ? data[SEED_KEY] : void 0;
|
|
21
|
+
}
|
|
22
|
+
function cookiePersist(locale) {
|
|
23
|
+
if (typeof document === "undefined") return;
|
|
24
|
+
try {
|
|
25
|
+
document.cookie = `locale=${locale};path=/;max-age=31536000;samesite=lax`;
|
|
26
|
+
} catch {
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function createI18n(config) {
|
|
30
|
+
const persist = config.persist ?? cookiePersist;
|
|
31
|
+
const [clientLocale, setClientLocale] = createSignal(readSeed() ?? config.locale);
|
|
32
|
+
const serverCtx = () => IS_SERVER ? getRequestContext() : void 0;
|
|
33
|
+
function locale() {
|
|
34
|
+
const ctx = serverCtx();
|
|
35
|
+
if (ctx) return ctx.__i18nLocale ?? config.locale;
|
|
36
|
+
return clientLocale();
|
|
37
|
+
}
|
|
38
|
+
function setLocale(next) {
|
|
39
|
+
const ctx = serverCtx();
|
|
40
|
+
if (ctx) {
|
|
41
|
+
ctx.__i18nLocale = next;
|
|
42
|
+
ctx.data?.set?.(SEED_KEY, next);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
setClientLocale(() => next);
|
|
46
|
+
persist(next);
|
|
47
|
+
}
|
|
48
|
+
function resolve(loc, key) {
|
|
49
|
+
const bundle = config.messages[loc];
|
|
50
|
+
if (bundle == null) return void 0;
|
|
51
|
+
const flat = bundle[key];
|
|
52
|
+
if (flat !== void 0) return flat;
|
|
53
|
+
let node = bundle;
|
|
54
|
+
for (const part of key.split(".")) {
|
|
55
|
+
if (node == null || typeof node !== "object") return void 0;
|
|
56
|
+
node = node[part];
|
|
57
|
+
}
|
|
58
|
+
return node;
|
|
59
|
+
}
|
|
60
|
+
function interpolate(str, params) {
|
|
61
|
+
if (!params) return str;
|
|
62
|
+
return str.replace(/\{(\w+)\}/g, (_m, k) => k in params ? String(params[k]) : `{${k}}`);
|
|
63
|
+
}
|
|
64
|
+
function t(key, params) {
|
|
65
|
+
const loc = locale();
|
|
66
|
+
let msg = resolve(loc, key);
|
|
67
|
+
if (msg === void 0 && loc !== config.locale) msg = resolve(config.locale, key);
|
|
68
|
+
if (msg === void 0) return key;
|
|
69
|
+
if (typeof msg !== "string") {
|
|
70
|
+
const count = Number(params?.count ?? 0);
|
|
71
|
+
const category = new Intl.PluralRules(loc).select(count);
|
|
72
|
+
const branch = msg[category] ?? msg.other ?? Object.values(msg)[0] ?? "";
|
|
73
|
+
return interpolate(branch, params);
|
|
74
|
+
}
|
|
75
|
+
return interpolate(msg, params);
|
|
76
|
+
}
|
|
77
|
+
const n = (value, options) => new Intl.NumberFormat(locale(), options).format(value);
|
|
78
|
+
const d = (value, options) => new Intl.DateTimeFormat(locale(), options).format(value);
|
|
79
|
+
const rt = (value, options = {}) => {
|
|
80
|
+
const { now = Date.now(), ...format } = options;
|
|
81
|
+
const seconds = (+value - +now) / 1e3;
|
|
82
|
+
const [unit, size] = RELATIVE_UNITS.find(([, size2]) => Math.abs(seconds) >= size2) ?? ["second", 1];
|
|
83
|
+
return new Intl.RelativeTimeFormat(locale(), { numeric: "auto", ...format }).format(Math.round(seconds / size), unit);
|
|
84
|
+
};
|
|
85
|
+
return { t, n, d, rt, locale, setLocale, locales: config.locales };
|
|
86
|
+
}
|
|
87
|
+
function detectLocale(acceptLanguage, locales) {
|
|
88
|
+
if (!acceptLanguage) return void 0;
|
|
89
|
+
const ranked = acceptLanguage.split(",").map((part) => {
|
|
90
|
+
const [tag, q] = part.trim().split(";q=");
|
|
91
|
+
return { tag: tag.toLowerCase(), q: q ? parseFloat(q) : 1 };
|
|
92
|
+
}).sort((a, b) => b.q - a.q);
|
|
93
|
+
for (const { tag } of ranked) {
|
|
94
|
+
const base = tag.split("-")[0];
|
|
95
|
+
const hit = locales.find((l) => l.toLowerCase() === tag || l.toLowerCase() === base);
|
|
96
|
+
if (hit) return hit;
|
|
97
|
+
}
|
|
98
|
+
return void 0;
|
|
99
|
+
}
|
|
100
|
+
export {
|
|
101
|
+
createI18n,
|
|
102
|
+
detectLocale
|
|
103
|
+
};
|