@astroscope/i18n 2.1.1 → 3.0.0
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/README.md +26 -35
- package/dist/{babel-plugin-CWafmw5z.js → babel-plugin-DJZS9L_W.js} +28 -30
- package/dist/babel-plugin-DJZS9L_W.js.map +1 -0
- package/dist/client/translate.d.ts +2 -2
- package/dist/client/translate.js +3 -3
- package/dist/client/translate.js.map +1 -1
- package/dist/context-BCUJxb8p.js +19 -0
- package/dist/context-BCUJxb8p.js.map +1 -0
- package/dist/extraction/index.d.ts +5 -2
- package/dist/extraction/index.d.ts.map +1 -1
- package/dist/extraction/index.js +2 -2
- package/dist/{context-CgqIoo00.js → i18n-C47eQi8U.js} +34 -44
- package/dist/i18n-C47eQi8U.js.map +1 -0
- package/dist/{i18n-Czkrfb_4.d.ts → i18n-C4nlkQTf.d.ts} +19 -9
- package/dist/i18n-C4nlkQTf.d.ts.map +1 -0
- package/dist/index.d.ts +14 -60
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +22 -14
- package/dist/index.js.map +1 -1
- package/dist/internal.d.ts +2 -2
- package/dist/internal.js +2 -1
- package/dist/{manifest-Cma68XM-.js → manifest-X-UhW6tH.js} +2 -2
- package/dist/manifest-X-UhW6tH.js.map +1 -0
- package/dist/{middleware-OnRVbxvs.js → middleware-BSEtDRYy.js} +21 -23
- package/dist/middleware-BSEtDRYy.js.map +1 -0
- package/dist/{rich-CHisXE-A.js → rich-BDqjhv2h.js} +31 -20
- package/dist/rich-BDqjhv2h.js.map +1 -0
- package/dist/server/chunk-middleware-entrypoint.js +1 -1
- package/dist/server/locale-middleware-entrypoint.d.ts +4 -0
- package/dist/server/locale-middleware-entrypoint.d.ts.map +1 -0
- package/dist/server/locale-middleware-entrypoint.js +8 -0
- package/dist/server/locale-middleware-entrypoint.js.map +1 -0
- package/dist/server/translate.d.ts +2 -2
- package/dist/server/translate.d.ts.map +1 -1
- package/dist/server/translate.js +4 -3
- package/dist/server/translate.js.map +1 -1
- package/dist/{types-f-SNJdvf.d.ts → types-BC-tQ30r.d.ts} +2 -2
- package/dist/{types-f-SNJdvf.d.ts.map → types-BC-tQ30r.d.ts.map} +1 -1
- package/dist/{types-DU8QHch4.d.ts → types-BjTD5hUT.d.ts} +6 -4
- package/dist/{types-DU8QHch4.d.ts.map → types-BjTD5hUT.d.ts.map} +1 -1
- package/dist/{types-Bo4jMK8g.d.ts → types-LT4Zo0bM.d.ts} +7 -1
- package/dist/types-LT4Zo0bM.d.ts.map +1 -0
- package/dist/{vite-plugin-C-jEpd8t.js → vite-plugin-CZqtAqQM.js} +83 -73
- package/dist/vite-plugin-CZqtAqQM.js.map +1 -0
- package/package.json +14 -9
- package/dist/babel-plugin-CWafmw5z.js.map +0 -1
- package/dist/context-CgqIoo00.js.map +0 -1
- package/dist/i18n-Czkrfb_4.d.ts.map +0 -1
- package/dist/manifest-Cma68XM-.js.map +0 -1
- package/dist/middleware-OnRVbxvs.js.map +0 -1
- package/dist/rich-CHisXE-A.js.map +0 -1
- package/dist/types-Bo4jMK8g.d.ts.map +0 -1
- package/dist/vite-plugin-C-jEpd8t.js.map +0 -1
package/dist/index.d.ts
CHANGED
|
@@ -1,70 +1,24 @@
|
|
|
1
|
-
import { a as TranslationMeta, r as RawTranslations } from "./types-
|
|
2
|
-
import { n as ExtractedKey, r as ExtractionManifest } from "./types-
|
|
3
|
-
import { n as I18nOptions } from "./types-
|
|
4
|
-
import { n as
|
|
5
|
-
import {
|
|
6
|
-
import { APIContext, AstroIntegration, MiddlewareHandler } from "astro";
|
|
1
|
+
import { a as TranslationMeta, r as RawTranslations } from "./types-BjTD5hUT.js";
|
|
2
|
+
import { n as ExtractedKey, r as ExtractionManifest } from "./types-BC-tQ30r.js";
|
|
3
|
+
import { n as I18nOptions } from "./types-LT4Zo0bM.js";
|
|
4
|
+
import { i as FallbackBehavior, n as LocaleResolver, r as i18n, t as I18nConfig } from "./i18n-C4nlkQTf.js";
|
|
5
|
+
import { AstroIntegration } from "astro";
|
|
7
6
|
//#region src/integration/integration.d.ts
|
|
8
7
|
/**
|
|
9
8
|
* Astro integration for i18n with automatic per-chunk translation loading.
|
|
10
9
|
*
|
|
11
10
|
* Registers the vite plugin for translation key extraction, chunk mapping and
|
|
12
|
-
* loader injection, and injects the chunk middleware serving
|
|
13
|
-
* translation chunks
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
11
|
+
* loader injection, and injects both middlewares: the chunk middleware serving
|
|
12
|
+
* `/_i18n/` translation chunks ahead of the project's own middleware, and the
|
|
13
|
+
* locale middleware after it, so the locale resolver from `i18n.configure` sees
|
|
14
|
+
* what a session middleware put on `locals`. Client state delivery and
|
|
15
|
+
* translation preloading happen at runtime: the locale middleware bootstraps
|
|
16
|
+
* `window.__i18n__`, and the islands emitter (via `@astroscope/node`) slices
|
|
17
|
+
* hashes and preloads translation chunks per island — plain `client:*`
|
|
18
|
+
* directives, no forks.
|
|
17
19
|
*/
|
|
18
20
|
export default function i18nIntegration(options?: I18nOptions): AstroIntegration;
|
|
19
21
|
//#endregion
|
|
20
|
-
//#region src/server/middleware.d.ts
|
|
21
|
-
type I18nMiddlewareOptions = {
|
|
22
|
-
locale: (ctx: APIContext) => string;
|
|
23
|
-
/**
|
|
24
|
-
* Patterns to exclude from locale context setup.
|
|
25
|
-
* Defaults to RECOMMENDED_EXCLUDES if not provided.
|
|
26
|
-
*
|
|
27
|
-
* @example
|
|
28
|
-
* ```ts
|
|
29
|
-
* import { RECOMMENDED_EXCLUDES } from '@astroscope/node/excludes';
|
|
30
|
-
*
|
|
31
|
-
* // extend defaults
|
|
32
|
-
* createI18nMiddleware({
|
|
33
|
-
* locale: (ctx) => ctx.locals.session?.locale ?? 'en',
|
|
34
|
-
* exclude: [...RECOMMENDED_EXCLUDES, { exact: '/health' }],
|
|
35
|
-
* })
|
|
36
|
-
*
|
|
37
|
-
* // disable excludes entirely
|
|
38
|
-
* createI18nMiddleware({
|
|
39
|
-
* locale: (ctx) => ctx.locals.session?.locale ?? 'en',
|
|
40
|
-
* exclude: [],
|
|
41
|
-
* })
|
|
42
|
-
* ```
|
|
43
|
-
*/
|
|
44
|
-
exclude?: ExcludePattern[] | ((context: APIContext) => boolean) | undefined;
|
|
45
|
-
};
|
|
46
|
-
/**
|
|
47
|
-
* Create the i18n locale middleware.
|
|
48
|
-
*
|
|
49
|
-
* Sets up the request context with locale and translations for `t()`, injects
|
|
50
|
-
* the client i18n state into html responses, and records the locale for the
|
|
51
|
-
* islands emitter. Place this after session middleware if your locale detection
|
|
52
|
-
* depends on session/cookies.
|
|
53
|
-
*
|
|
54
|
-
* @example
|
|
55
|
-
* ```typescript
|
|
56
|
-
* import { createI18nMiddleware, detectLocale, i18n } from '@astroscope/i18n';
|
|
57
|
-
*
|
|
58
|
-
* export const i18nMiddleware = createI18nMiddleware({
|
|
59
|
-
* locale: (ctx) =>
|
|
60
|
-
* ctx.cookies.get('locale')?.value ??
|
|
61
|
-
* detectLocale(ctx.request) ??
|
|
62
|
-
* i18n.getConfig().defaultLocale,
|
|
63
|
-
* });
|
|
64
|
-
* ```
|
|
65
|
-
*/
|
|
66
|
-
export declare function createI18nMiddleware(options: I18nMiddlewareOptions): MiddlewareHandler;
|
|
67
|
-
//#endregion
|
|
68
22
|
//#region src/server/locale.d.ts
|
|
69
23
|
/**
|
|
70
24
|
* Detect locale from Accept-Language header.
|
|
@@ -72,5 +26,5 @@ export declare function createI18nMiddleware(options: I18nMiddlewareOptions): Mi
|
|
|
72
26
|
*/
|
|
73
27
|
export declare function detectLocale(request: Request): string | undefined;
|
|
74
28
|
//#endregion
|
|
75
|
-
export { type ExtractedKey, type ExtractionManifest, type FallbackBehavior, type I18nConfig, type
|
|
29
|
+
export { type ExtractedKey, type ExtractionManifest, type FallbackBehavior, type I18nConfig, type I18nOptions, type LocaleResolver, type RawTranslations, type TranslationMeta, i18n };
|
|
76
30
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","names":[],"sources":["../src/integration/integration.ts","../src/server/
|
|
1
|
+
{"version":3,"file":"index.d.ts","names":[],"sources":["../src/integration/integration.ts","../src/server/locale.ts"],"mappings":";;;;;;;;;;;;;;;;;;;wBAiBwB,gBAAgB,UAAS,cAAmB;;;;;;;wBCXpD,aAAa,SAAS"}
|
package/dist/index.js
CHANGED
|
@@ -1,19 +1,21 @@
|
|
|
1
|
-
import { t as i18nVitePlugin } from "./vite-plugin-
|
|
2
|
-
import {
|
|
3
|
-
import { n as createI18nMiddleware } from "./middleware-OnRVbxvs.js";
|
|
1
|
+
import { t as i18nVitePlugin } from "./vite-plugin-CZqtAqQM.js";
|
|
2
|
+
import { t as i18n } from "./i18n-C47eQi8U.js";
|
|
4
3
|
//#region src/integration/integration.ts
|
|
5
4
|
/**
|
|
6
5
|
* Astro integration for i18n with automatic per-chunk translation loading.
|
|
7
6
|
*
|
|
8
7
|
* Registers the vite plugin for translation key extraction, chunk mapping and
|
|
9
|
-
* loader injection, and injects the chunk middleware serving
|
|
10
|
-
* translation chunks
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
8
|
+
* loader injection, and injects both middlewares: the chunk middleware serving
|
|
9
|
+
* `/_i18n/` translation chunks ahead of the project's own middleware, and the
|
|
10
|
+
* locale middleware after it, so the locale resolver from `i18n.configure` sees
|
|
11
|
+
* what a session middleware put on `locals`. Client state delivery and
|
|
12
|
+
* translation preloading happen at runtime: the locale middleware bootstraps
|
|
13
|
+
* `window.__i18n__`, and the islands emitter (via `@astroscope/node`) slices
|
|
14
|
+
* hashes and preloads translation chunks per island — plain `client:*`
|
|
15
|
+
* directives, no forks.
|
|
14
16
|
*/
|
|
15
17
|
function i18nIntegration(options = {}) {
|
|
16
|
-
const { consistency = "error" } = options;
|
|
18
|
+
const { consistency = "error", exclude = null } = options;
|
|
17
19
|
return {
|
|
18
20
|
name: "@astroscope/i18n",
|
|
19
21
|
hooks: { "astro:config:setup": ({ addMiddleware, updateConfig, logger }) => {
|
|
@@ -21,9 +23,14 @@ function i18nIntegration(options = {}) {
|
|
|
21
23
|
order: "pre",
|
|
22
24
|
entrypoint: "@astroscope/i18n/chunk-middleware"
|
|
23
25
|
});
|
|
26
|
+
addMiddleware({
|
|
27
|
+
order: "post",
|
|
28
|
+
entrypoint: "@astroscope/i18n/locale-middleware"
|
|
29
|
+
});
|
|
24
30
|
updateConfig({ vite: { plugins: [i18nVitePlugin({
|
|
25
31
|
logger,
|
|
26
|
-
consistency
|
|
32
|
+
consistency,
|
|
33
|
+
exclude
|
|
27
34
|
})] } });
|
|
28
35
|
} }
|
|
29
36
|
};
|
|
@@ -42,13 +49,14 @@ function detectLocale(request) {
|
|
|
42
49
|
const preferred = acceptLanguage.split(",").map((part) => {
|
|
43
50
|
const [lang, q = "q=1"] = part.split(";").map((s) => s.trim());
|
|
44
51
|
return {
|
|
45
|
-
lang: lang?.split("-")[0]?.toLowerCase(),
|
|
46
|
-
q: parseFloat(q.replace("q=", ""))
|
|
52
|
+
lang: lang?.split("-", 1)[0]?.toLowerCase(),
|
|
53
|
+
q: Number.parseFloat(q.replace("q=", ""))
|
|
47
54
|
};
|
|
48
|
-
}).filter((p) => !!p.lang)
|
|
55
|
+
}).filter((p) => !!p.lang);
|
|
56
|
+
preferred.sort((a, b) => b.q - a.q);
|
|
49
57
|
for (const { lang } of preferred) if (locales.includes(lang)) return lang;
|
|
50
58
|
}
|
|
51
59
|
//#endregion
|
|
52
|
-
export {
|
|
60
|
+
export { i18nIntegration as default, detectLocale, i18n };
|
|
53
61
|
|
|
54
62
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../src/integration/integration.ts","../src/server/locale.ts"],"sourcesContent":["import type { AstroIntegration } from 'astro';\nimport { i18nVitePlugin } from '../extraction/vite-plugin.js';\nimport type { I18nOptions } from './types.js';\n\n/**\n * Astro integration for i18n with automatic per-chunk translation loading.\n *\n * Registers the vite plugin for translation key extraction, chunk mapping and\n * loader injection, and injects the chunk middleware serving `/_i18n
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../src/integration/integration.ts","../src/server/locale.ts"],"sourcesContent":["import type { AstroIntegration } from 'astro';\nimport { i18nVitePlugin } from '../extraction/vite-plugin.js';\nimport type { I18nOptions } from './types.js';\n\n/**\n * Astro integration for i18n with automatic per-chunk translation loading.\n *\n * Registers the vite plugin for translation key extraction, chunk mapping and\n * loader injection, and injects both middlewares: the chunk middleware serving\n * `/_i18n/` translation chunks ahead of the project's own middleware, and the\n * locale middleware after it, so the locale resolver from `i18n.configure` sees\n * what a session middleware put on `locals`. Client state delivery and\n * translation preloading happen at runtime: the locale middleware bootstraps\n * `window.__i18n__`, and the islands emitter (via `@astroscope/node`) slices\n * hashes and preloads translation chunks per island — plain `client:*`\n * directives, no forks.\n */\nexport default function i18nIntegration(options: I18nOptions = {}): AstroIntegration {\n const { consistency = 'error', exclude = null } = options;\n\n return {\n name: '@astroscope/i18n',\n hooks: {\n 'astro:config:setup': ({ addMiddleware, updateConfig, logger }) => {\n addMiddleware({ order: 'pre', entrypoint: '@astroscope/i18n/chunk-middleware' });\n addMiddleware({ order: 'post', entrypoint: '@astroscope/i18n/locale-middleware' });\n\n updateConfig({\n vite: {\n plugins: [i18nVitePlugin({ logger, consistency, exclude })],\n },\n });\n },\n },\n };\n}\n","import { i18n } from './i18n.js';\n\n/**\n * Detect locale from Accept-Language header.\n * Returns best match from configured locales, or undefined if none match.\n */\nexport function detectLocale(request: Request): string | undefined {\n if (!i18n.isConfigured()) return undefined;\n\n const { locales } = i18n.getConfig();\n const acceptLanguage = request.headers.get('accept-language');\n\n if (!acceptLanguage) return undefined;\n\n const preferred = acceptLanguage\n .split(',')\n .map((part) => {\n // optional whitespace around `;` is legal per rfc 9110\n const [lang, q = 'q=1'] = part.split(';').map((s) => s.trim());\n const baseLang = lang?.split('-', 1)[0]?.toLowerCase();\n\n return {\n lang: baseLang,\n // eslint-disable-next-line unicorn/prefer-number-coercion -- Number() differs on malformed q values ('' → 0, '0.8x' → NaN)\n q: Number.parseFloat(q.replace('q=', '')),\n };\n })\n .filter((p): p is { lang: string; q: number } => !!p.lang);\n\n preferred.sort((a, b) => b.q - a.q);\n\n for (const { lang } of preferred) {\n if (locales.includes(lang)) {\n return lang;\n }\n }\n\n return undefined;\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAiBA,SAAwB,gBAAgB,UAAuB,CAAC,GAAqB;CACnF,MAAM,EAAE,cAAc,SAAS,UAAU,SAAS;CAElD,OAAO;EACL,MAAM;EACN,OAAO,EACL,uBAAuB,EAAE,eAAe,cAAc,aAAa;GACjE,cAAc;IAAE,OAAO;IAAO,YAAY;GAAoC,CAAC;GAC/E,cAAc;IAAE,OAAO;IAAQ,YAAY;GAAqC,CAAC;GAEjF,aAAa,EACX,MAAM,EACJ,SAAS,CAAC,eAAe;IAAE;IAAQ;IAAa;GAAQ,CAAC,CAAC,EAC5D,EACF,CAAC;EACH,EACF;CACF;AACF;;;;;;;AC7BA,SAAgB,aAAa,SAAsC;CACjE,IAAI,CAAC,KAAK,aAAa,GAAG,OAAO,KAAA;CAEjC,MAAM,EAAE,YAAY,KAAK,UAAU;CACnC,MAAM,iBAAiB,QAAQ,QAAQ,IAAI,iBAAiB;CAE5D,IAAI,CAAC,gBAAgB,OAAO,KAAA;CAE5B,MAAM,YAAY,eACf,MAAM,GAAG,CAAC,CACV,KAAK,SAAS;EAEb,MAAM,CAAC,MAAM,IAAI,SAAS,KAAK,MAAM,GAAG,CAAC,CAAC,KAAK,MAAM,EAAE,KAAK,CAAC;EAG7D,OAAO;GACL,MAHe,MAAM,MAAM,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,YAAY;GAKnD,GAAG,OAAO,WAAW,EAAE,QAAQ,MAAM,EAAE,CAAC;EAC1C;CACF,CAAC,CAAC,CACD,QAAQ,MAAwC,CAAC,CAAC,EAAE,IAAI;CAE3D,UAAU,MAAM,GAAG,MAAM,EAAE,IAAI,EAAE,CAAC;CAElC,KAAK,MAAM,EAAE,UAAU,WACrB,IAAI,QAAQ,SAAS,IAAI,GACvB,OAAO;AAKb"}
|
package/dist/internal.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { n as I18nClientState } from "./types-
|
|
2
|
-
import {
|
|
1
|
+
import { n as I18nClientState } from "./types-BjTD5hUT.js";
|
|
2
|
+
import { a as I18nContext, r as i18n } from "./i18n-C4nlkQTf.js";
|
|
3
3
|
//#region src/server/context.d.ts
|
|
4
4
|
/**
|
|
5
5
|
* Get the current i18n context
|
package/dist/internal.js
CHANGED
|
@@ -8,7 +8,7 @@ import path from "node:path";
|
|
|
8
8
|
const I18N_MANIFEST_GLOBAL_KEY = "__astroscope_i18n_manifest__";
|
|
9
9
|
function getGlobalState() {
|
|
10
10
|
const g = globalThis;
|
|
11
|
-
|
|
11
|
+
g[I18N_MANIFEST_GLOBAL_KEY] ??= {
|
|
12
12
|
extractedKeys: [],
|
|
13
13
|
chunkManifest: {},
|
|
14
14
|
scriptChunks: [],
|
|
@@ -41,4 +41,4 @@ function getManifest() {
|
|
|
41
41
|
//#endregion
|
|
42
42
|
export { getManifest as n, getGlobalState as t };
|
|
43
43
|
|
|
44
|
-
//# sourceMappingURL=manifest-
|
|
44
|
+
//# sourceMappingURL=manifest-X-UhW6tH.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"manifest-X-UhW6tH.js","names":[],"sources":["../src/extraction/manifest.ts"],"sourcesContent":["/**\n * Manifest state management for i18n extraction\n *\n * Separated from vite-plugin to avoid bundling Babel at runtime\n */\n\nimport path from 'node:path';\nimport type { ChunkManifest, ExtractedKey, ExtractionManifest } from './types.js';\n\n// global state for dev mode live access via globalThis\n// this allows the virtual module to access current extraction data\n// even after initial import (since dev mode transforms are on-demand)\nconst I18N_MANIFEST_GLOBAL_KEY = '__astroscope_i18n_manifest__';\n\nexport type GlobalI18nState = {\n extractedKeys: ExtractedKey[];\n chunkManifest: ChunkManifest;\n scriptChunks: string[];\n\n // project root for relative paths\n projectRoot: string;\n\n // incremented on each sync to signal cache invalidation\n version: number;\n};\n\nexport function getGlobalState(): GlobalI18nState {\n const g = globalThis as Record<string, unknown>;\n\n g[I18N_MANIFEST_GLOBAL_KEY] ??= {\n extractedKeys: [],\n chunkManifest: {},\n scriptChunks: [],\n projectRoot: '',\n version: 0,\n } satisfies GlobalI18nState;\n\n return g[I18N_MANIFEST_GLOBAL_KEY] as GlobalI18nState;\n}\n\n/**\n * Get the current extraction manifest (for dev mode live access).\n * This bypasses Vite's module caching to return fresh data.\n */\nexport function getManifest(): ExtractionManifest {\n const state = getGlobalState();\n\n const keys = state.extractedKeys.map((key) => ({\n ...key,\n // make file paths relative to project root for better readability\n // and avoiding leaking absolute paths from dev or ci environments\n files: key.files.map((fileLocation) => {\n // split on the trailing `:line` only — the path itself may contain colons (windows drive letters)\n const match = /^(.+):(\\d+)$/.exec(fileLocation);\n const [filePath, line] = match ? [match[1], match[2]] : [fileLocation, undefined];\n\n if (!filePath) return fileLocation;\n\n const relativePath = state.projectRoot ? path.relative(state.projectRoot, filePath) : filePath;\n\n return line ? `${relativePath}:${line}` : relativePath;\n }),\n }));\n\n return { keys, chunks: state.chunkManifest, scripts: state.scriptChunks };\n}\n"],"mappings":";;;;;;;AAYA,MAAM,2BAA2B;AAcjC,SAAgB,iBAAkC;CAChD,MAAM,IAAI;CAEV,EAAE,8BAA8B;EAC9B,eAAe,CAAC;EAChB,eAAe,CAAC;EAChB,cAAc,CAAC;EACf,aAAa;EACb,SAAS;CACX;CAEA,OAAO,EAAE;AACX;;;;;AAMA,SAAgB,cAAkC;CAChD,MAAM,QAAQ,eAAe;CAmB7B,OAAO;EAAE,MAjBI,MAAM,cAAc,KAAK,SAAS;GAC7C,GAAG;GAGH,OAAO,IAAI,MAAM,KAAK,iBAAiB;IAErC,MAAM,QAAQ,eAAe,KAAK,YAAY;IAC9C,MAAM,CAAC,UAAU,QAAQ,QAAQ,CAAC,MAAM,IAAI,MAAM,EAAE,IAAI,CAAC,cAAc,KAAA,CAAS;IAEhF,IAAI,CAAC,UAAU,OAAO;IAEtB,MAAM,eAAe,MAAM,cAAc,KAAK,SAAS,MAAM,aAAa,QAAQ,IAAI;IAEtF,OAAO,OAAO,GAAG,aAAa,GAAG,SAAS;GAC5C,CAAC;EACH,EAEY;EAAG,QAAQ,MAAM;EAAe,SAAS,MAAM;CAAa;AAC1E"}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { t as buildI18nChunkUrl } from "./url-CifeaDzl.js";
|
|
2
|
-
import {
|
|
2
|
+
import { t as i18n } from "./i18n-C47eQi8U.js";
|
|
3
|
+
import { n as runWithContext } from "./context-BCUJxb8p.js";
|
|
3
4
|
import { RECOMMENDED_EXCLUDES, shouldExclude } from "@astroscope/node/excludes";
|
|
4
5
|
import { log, overrideRequestRoute } from "@astroscope/node/log";
|
|
5
6
|
import { registerDocumentEmitter, registerIslandEmitter } from "@astroscope/node/islands";
|
|
@@ -12,7 +13,7 @@ import { registerDocumentEmitter, registerIslandEmitter } from "@astroscope/node
|
|
|
12
13
|
*/
|
|
13
14
|
/** translation values may contain `<\/script>` — escape `<` so they cannot end the tag */
|
|
14
15
|
function jsonForScript(value) {
|
|
15
|
-
return JSON.stringify(value).
|
|
16
|
+
return JSON.stringify(value).replaceAll("<", String.raw`\u003c`);
|
|
16
17
|
}
|
|
17
18
|
/** the full dev-mode state: every translation for the locale */
|
|
18
19
|
function createFullStateScript(locale) {
|
|
@@ -141,7 +142,8 @@ function registerI18nEmitters() {
|
|
|
141
142
|
}
|
|
142
143
|
const merge = {};
|
|
143
144
|
let fresh = false;
|
|
144
|
-
for (const [chunk, hash] of chunks)
|
|
145
|
+
for (const [chunk, hash] of chunks) {
|
|
146
|
+
if (emitted.has(chunk)) continue;
|
|
145
147
|
emitted.add(chunk);
|
|
146
148
|
merge[chunk] = hash;
|
|
147
149
|
fresh = true;
|
|
@@ -216,43 +218,39 @@ function createI18nChunkMiddleware() {
|
|
|
216
218
|
/**
|
|
217
219
|
* Create the i18n locale middleware.
|
|
218
220
|
*
|
|
219
|
-
*
|
|
220
|
-
* the
|
|
221
|
-
*
|
|
222
|
-
*
|
|
221
|
+
* Resolves the request's locale through the resolver from `i18n.configure({ locale })`
|
|
222
|
+
* (the default locale when there is none or it returns `undefined`), sets up the
|
|
223
|
+
* request context for `t()`, records the locale for the islands emitter, which
|
|
224
|
+
* delivers the client state, and runs the rest of the chain inside that context.
|
|
223
225
|
*
|
|
224
|
-
*
|
|
225
|
-
*
|
|
226
|
-
*
|
|
226
|
+
* Injected by the integration as `post` middleware, after the project's own, so the
|
|
227
|
+
* resolver sees whatever a session middleware put on `locals` — not part of the
|
|
228
|
+
* public API.
|
|
227
229
|
*
|
|
228
|
-
*
|
|
229
|
-
* locale: (ctx) =>
|
|
230
|
-
* ctx.cookies.get('locale')?.value ??
|
|
231
|
-
* detectLocale(ctx.request) ??
|
|
232
|
-
* i18n.getConfig().defaultLocale,
|
|
233
|
-
* });
|
|
234
|
-
* ```
|
|
230
|
+
* @internal
|
|
235
231
|
*/
|
|
236
|
-
function
|
|
232
|
+
function createI18nLocaleMiddleware(options = {}) {
|
|
237
233
|
registerI18nEmitters();
|
|
234
|
+
const exclude = options.exclude ?? RECOMMENDED_EXCLUDES;
|
|
238
235
|
return (ctx, next) => {
|
|
239
|
-
if (shouldExclude(ctx,
|
|
236
|
+
if (shouldExclude(ctx, exclude)) return next();
|
|
240
237
|
if (!i18n.isConfigured()) {
|
|
241
238
|
log.error("i18n not configured, passing through");
|
|
242
239
|
return next();
|
|
243
240
|
}
|
|
244
|
-
const
|
|
241
|
+
const config = i18n.getConfig();
|
|
242
|
+
const locale = config.locale?.(ctx) ?? config.defaultLocale;
|
|
245
243
|
setRequestLocale(ctx, locale);
|
|
246
244
|
const context = {
|
|
247
245
|
locale,
|
|
248
246
|
translations: i18n.getCompiledTranslations(locale),
|
|
249
247
|
rawTranslations: i18n.getTranslations(locale),
|
|
250
|
-
fallback:
|
|
248
|
+
fallback: config.fallback
|
|
251
249
|
};
|
|
252
250
|
return runWithContext(context, () => next());
|
|
253
251
|
};
|
|
254
252
|
}
|
|
255
253
|
//#endregion
|
|
256
|
-
export {
|
|
254
|
+
export { createI18nLocaleMiddleware as n, createI18nChunkMiddleware as t };
|
|
257
255
|
|
|
258
|
-
//# sourceMappingURL=middleware-
|
|
256
|
+
//# sourceMappingURL=middleware-BSEtDRYy.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"middleware-BSEtDRYy.js","names":[],"sources":["../src/server/client-state.ts","../src/server/islands-emitter.ts","../src/server/middleware.ts"],"sourcesContent":["import { i18n } from './i18n.js';\n\n/**\n * Scripts bootstrapping `window.__i18n__` on html responses, delivered through the\n * `@astroscope/node` document emitter (see islands-emitter.ts for the placement\n * policy). Both writers merge orderlessly (`??=` + assign), so emitter scripts and\n * the bootstrap can arrive in either order.\n */\n\n/** translation values may contain `</script>` — escape `<` so they cannot end the tag */\nexport function jsonForScript(value: unknown): string {\n return JSON.stringify(value).replaceAll('<', String.raw`\\u003c`);\n}\n\n/** the full dev-mode state: every translation for the locale */\nexport function createFullStateScript(locale: string): string {\n const translations = i18n.getTranslations(locale);\n const state = jsonForScript({ locale, hashes: {}, translations });\n\n return `<script>window.__i18n__=Object.assign(window.__i18n__??{hashes:{},translations:{}},${state});</script>`;\n}\n\n/** the prod bootstrap: chunk hashes for astro `<script>` entry consumers */\nexport function createScriptHashesScript(locale: string): string | null {\n const hashes = i18n.getScriptChunkHashes(locale);\n\n if (Object.keys(hashes).length === 0) {\n return null;\n }\n\n const init = jsonForScript({ locale, hashes: {}, translations: {} });\n\n return `<script>{const i=window.__i18n__??=${init};Object.assign(i.hashes,${jsonForScript(hashes)});}</script>`;\n}\n","import {\n type DocumentEmitter,\n type IslandInfo,\n registerDocumentEmitter,\n registerIslandEmitter,\n} from '@astroscope/node/islands';\nimport type { APIContext } from 'astro';\nimport { buildI18nChunkUrl } from '../shared/url.js';\nimport { createFullStateScript, createScriptHashesScript, jsonForScript } from './client-state.js';\nimport { i18n } from './i18n.js';\n\n/**\n * Contributes the i18n slice for each island the `@astroscope/node` islands\n * middleware streams past:\n *\n * - an inline merge script with the hashes of the island's full chunk closure —\n * dynamic imports included, since a `React.lazy` chunk's injected loader reads\n * its hash at execution time. Emitted before the island tag, it is parsed\n * strictly before the island connects, and the island's chunks execute at\n * least a network round-trip later — so each island's hashes win their race\n * even while the rest of the document is still streaming. Chunks already\n * covered earlier in the document are skipped.\n * - translation-chunk preload links for the static closure (link tags on\n * immediate islands).\n * - translation-chunk eager imports for the full closure, dynamic chunks\n * included, fired by the deferred gate: the chunks are idempotent data\n * modules, so early evaluation is free and the loader's awaited import hits\n * the module cache instead of fetching after the component graph evaluated.\n * No per-document dedup — a shared chunk is covered by whichever island's\n * gate fires first.\n *\n * The locale is recorded per request by the i18n middleware — the emitter runs\n * while the response streams, outside the middleware's AsyncLocalStorage scope.\n * It is keyed on `context.locals`, the one object astro keeps for the whole\n * request: `context.request` is replaced by every `next(url)` rewrite, once per\n * nesting level of `sequence`, so the request the middleware saw is not the one\n * the islands middleware holds when the emitters run.\n */\n\nconst requestLocales = new WeakMap<object, string>();\nconst emittedByRequest = new WeakMap<object, Set<string>>();\n\n// vite defines import.meta.env when bundling the middleware; typed structurally\n// so the package compiles without vite's global client types\nconst IS_DEV = !!(import.meta as { env?: { DEV?: boolean } }).env?.DEV;\n\nlet registered = false;\n\nexport function setRequestLocale(context: APIContext, locale: string): void {\n requestLocales.set(context.locals, locale);\n}\n\n/** public url of a chunk → manifest chunk name, e.g. `…/_astro/Cart.abc.js` → `Cart.abc` */\nfunction urlToChunkName(url: string): string {\n return url.slice(url.lastIndexOf('/') + 1).replace(/\\.js$/, '');\n}\n\n/** an island's translated chunks and their urls for one locale */\ntype IslandSlice = {\n /** full closure: what the merge script covers */\n chunks: [name: string, hash: string][];\n /** static closure: preload links */\n links: string[];\n /** full closure: eager imports */\n imports: string[];\n};\n\n// islands arrive as the same object per component/renderer/directive and the\n// hashes object is replaced whenever translations change, so keying on both\n// caches each slice for exactly as long as it is valid\nconst slices = new WeakMap<Record<string, string>, WeakMap<IslandInfo, IslandSlice>>();\nconst inits = new Map<string, string>();\n\nfunction sliceFor(island: IslandInfo, locale: string, hashes: Record<string, string>): IslandSlice {\n let byIsland = slices.get(hashes);\n\n if (!byIsland) {\n byIsland = new WeakMap();\n slices.set(hashes, byIsland);\n }\n\n let slice = byIsland.get(island);\n\n if (slice) {\n return slice;\n }\n\n const translated = (urls: string[]): [string, string][] => {\n const result: [string, string][] = [];\n\n for (const url of urls) {\n const chunk = urlToChunkName(url);\n const hash = hashes[chunk];\n\n if (hash) {\n result.push([chunk, hash]);\n }\n }\n\n return result;\n };\n\n const chunks = translated(island.fullClosure);\n\n slice = {\n chunks,\n links: translated(island.staticClosure).map(([chunk, hash]) => buildI18nChunkUrl(locale, chunk, hash)),\n imports: chunks.map(([chunk, hash]) => buildI18nChunkUrl(locale, chunk, hash)),\n };\n byIsland.set(island, slice);\n\n return slice;\n}\n\nfunction initFor(locale: string): string {\n let init = inits.get(locale);\n\n if (init === undefined) {\n init = jsonForScript({ locale, hashes: {}, translations: {} });\n inits.set(locale, init);\n }\n\n return init;\n}\n\n/**\n * The document-level bootstrap for `window.__i18n__`, exported for tests.\n */\nexport function createI18nDocumentEmitter(dev: boolean = IS_DEV): DocumentEmitter {\n return (context) => {\n if (!i18n.isConfigured()) {\n return null;\n }\n\n const locale = requestLocales.get(context.locals);\n\n if (locale === undefined) {\n return null;\n }\n\n if (dev) {\n return { head: createFullStateScript(locale) };\n }\n\n if (Object.keys(i18n.getManifest().chunks).length === 0) {\n return null;\n }\n\n return { end: () => createScriptHashesScript(locale) };\n };\n}\n\nexport function registerI18nEmitters(): void {\n if (registered) {\n return;\n }\n\n registered = true;\n\n registerDocumentEmitter(createI18nDocumentEmitter());\n\n registerIslandEmitter((island, context) => {\n // no request context (prerendered build pass) — the locale is unknowable\n if (!context || !i18n.isConfigured()) {\n return null;\n }\n\n const locale = requestLocales.get(context.locals);\n\n if (locale === undefined) {\n return null;\n }\n\n const hashes = i18n.getHashes(locale);\n const { chunks, links, imports } = sliceFor(island, locale, hashes);\n\n let emitted = emittedByRequest.get(context.locals);\n\n if (!emitted) {\n emitted = new Set();\n emittedByRequest.set(context.locals, emitted);\n }\n\n const merge: Record<string, string> = {};\n let fresh = false;\n\n for (const [chunk, hash] of chunks) {\n if (emitted.has(chunk)) {\n continue;\n }\n\n emitted.add(chunk);\n merge[chunk] = hash;\n fresh = true;\n }\n\n const html = fresh\n ? `<script>{const i=window.__i18n__??=${initFor(locale)};Object.assign(i.hashes,${jsonForScript(merge)});}</script>`\n : undefined;\n\n if (!html && links.length === 0 && imports.length === 0) {\n return null;\n }\n\n return {\n html,\n links: links.length > 0 ? links : undefined,\n imports: imports.length > 0 ? imports : undefined,\n };\n });\n}\n","import { type ExcludePattern, RECOMMENDED_EXCLUDES, shouldExclude } from '@astroscope/node/excludes';\nimport { log, overrideRequestRoute } from '@astroscope/node/log';\nimport type { MiddlewareHandler } from 'astro';\nimport { runWithContext } from './context.js';\nimport { i18n } from './i18n.js';\nimport { registerI18nEmitters, setRequestLocale } from './islands-emitter.js';\nimport type { I18nContext } from './types.js';\n\nexport type I18nLocaleMiddlewareOptions = {\n /** patterns to skip locale context setup for; defaults to RECOMMENDED_EXCLUDES */\n exclude?: ExcludePattern[] | undefined;\n};\n\nconst I18N_ENDPOINT_PREFIX = '/_i18n/';\n\n// astro has no route behind the chunk endpoint, so requests would otherwise be\n// logged and measured under whatever routing matched — typically `/404`\nconst I18N_ENDPOINT_ROUTE = '/_i18n/[locale]/[chunk]';\n\n/**\n * Serve a translation chunk. Returns `undefined` when the path isn't a chunk\n * request, and the caller passes it on to astro.\n */\nfunction createChunkResponse(pathname: string): Response | undefined {\n if (!pathname.startsWith(I18N_ENDPOINT_PREFIX)) {\n return undefined;\n }\n\n if (!i18n.isConfigured()) {\n log.error('i18n not configured, passing through');\n\n return undefined;\n }\n\n const path = pathname.slice(I18N_ENDPOINT_PREFIX.length);\n\n // {locale}/{chunkName}.{hash}.js — sliced by hand, this runs for every chunk request\n const slashIdx = path.indexOf('/');\n\n if (slashIdx === -1) {\n return undefined;\n }\n\n const locale = path.slice(0, slashIdx);\n\n // validate locale against configured locales to prevent arbitrary locale injection\n const config = i18n.getConfig();\n\n if (!config.locales.includes(locale)) {\n return new Response('/* unknown locale */', {\n status: 404,\n headers: { 'Content-Type': 'application/javascript' },\n });\n }\n\n const rest = path.slice(slashIdx + 1);\n\n if (!rest.endsWith('.js')) {\n return undefined;\n }\n\n const withoutJs = rest.slice(0, -3);\n const lastDotIdx = withoutJs.lastIndexOf('.');\n\n if (lastDotIdx === -1) {\n return undefined;\n }\n\n const chunkName = withoutJs.slice(0, lastDotIdx);\n const body = i18n.getChunkBody(locale, chunkName);\n\n if (!body) {\n return new Response(`/* chunk not found: ${chunkName} */`, {\n status: 404,\n headers: { 'Content-Type': 'application/javascript' },\n });\n }\n\n return new Response(body as BodyInit, {\n status: 200,\n headers: {\n 'Content-Type': 'text/javascript; charset=utf-8',\n 'Content-Length': String(body.byteLength),\n 'Cache-Control': 'public, max-age=31536000, immutable',\n },\n });\n}\n\n/**\n * Create the i18n chunk middleware that serves translation chunks at `/_i18n/` endpoints.\n *\n * Injected by the integration as `pre` middleware (before any user middleware) —\n * not part of the public API.\n *\n * @internal\n */\nexport function createI18nChunkMiddleware(): MiddlewareHandler {\n return (ctx, next) => {\n const response = createChunkResponse(ctx.url.pathname);\n\n if (!response) {\n return next();\n }\n\n // this middleware, not astro's routing, is what serves the request\n overrideRequestRoute(I18N_ENDPOINT_ROUTE);\n\n return response;\n };\n}\n\n/**\n * Create the i18n locale middleware.\n *\n * Resolves the request's locale through the resolver from `i18n.configure({ locale })`\n * (the default locale when there is none or it returns `undefined`), sets up the\n * request context for `t()`, records the locale for the islands emitter, which\n * delivers the client state, and runs the rest of the chain inside that context.\n *\n * Injected by the integration as `post` middleware, after the project's own, so the\n * resolver sees whatever a session middleware put on `locals` — not part of the\n * public API.\n *\n * @internal\n */\nexport function createI18nLocaleMiddleware(options: I18nLocaleMiddlewareOptions = {}): MiddlewareHandler {\n registerI18nEmitters();\n\n const exclude = options.exclude ?? RECOMMENDED_EXCLUDES;\n\n return (ctx, next) => {\n if (shouldExclude(ctx, exclude)) {\n return next();\n }\n\n if (!i18n.isConfigured()) {\n log.error('i18n not configured, passing through');\n\n return next();\n }\n\n const config = i18n.getConfig();\n const locale = config.locale?.(ctx) ?? config.defaultLocale;\n\n // the islands emitter runs while the response streams, outside this ALS scope\n setRequestLocale(ctx, locale);\n\n const context: I18nContext = {\n locale,\n translations: i18n.getCompiledTranslations(locale),\n rawTranslations: i18n.getTranslations(locale),\n fallback: config.fallback,\n };\n\n return runWithContext(context, () => next());\n };\n}\n"],"mappings":"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"}
|
|
@@ -32,6 +32,31 @@ function addKeyIfNeeded(element, key) {
|
|
|
32
32
|
function isMarkupPart(part) {
|
|
33
33
|
return part.type === "markup";
|
|
34
34
|
}
|
|
35
|
+
function applyMarkupPart(part, stack, result, components, nextKey) {
|
|
36
|
+
const target = stack.length > 0 ? stack.at(-1).children : result;
|
|
37
|
+
switch (part.kind) {
|
|
38
|
+
case "open":
|
|
39
|
+
stack.push({
|
|
40
|
+
name: part.name,
|
|
41
|
+
children: []
|
|
42
|
+
});
|
|
43
|
+
break;
|
|
44
|
+
case "close": {
|
|
45
|
+
const frame = stack.pop();
|
|
46
|
+
if (!frame) break;
|
|
47
|
+
const component = components[frame.name];
|
|
48
|
+
const parent = stack.length > 0 ? stack.at(-1).children : result;
|
|
49
|
+
if (component) parent.push(addKeyIfNeeded(component(frame.children), nextKey()));
|
|
50
|
+
else parent.push(...frame.children);
|
|
51
|
+
break;
|
|
52
|
+
}
|
|
53
|
+
case "standalone": {
|
|
54
|
+
const component = components[part.name];
|
|
55
|
+
if (component) target.push(addKeyIfNeeded(component([]), nextKey()));
|
|
56
|
+
break;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
35
60
|
/**
|
|
36
61
|
* Convert MF2 formatToParts() output to a tree of nodes.
|
|
37
62
|
* Works with any JSX runtime (Astro, React, etc.) - the callbacks
|
|
@@ -51,25 +76,11 @@ function partsToNodes(parts, components) {
|
|
|
51
76
|
const result = [];
|
|
52
77
|
const stack = [];
|
|
53
78
|
let keyIndex = 0;
|
|
79
|
+
const nextKey = () => `rich-${keyIndex++}`;
|
|
54
80
|
for (const part of parts) {
|
|
55
|
-
const target = stack.length > 0 ? stack
|
|
56
|
-
if (isMarkupPart(part))
|
|
57
|
-
|
|
58
|
-
name: part.name,
|
|
59
|
-
children: []
|
|
60
|
-
});
|
|
61
|
-
else if (part.kind === "close") {
|
|
62
|
-
const frame = stack.pop();
|
|
63
|
-
if (!frame) continue;
|
|
64
|
-
const component = components[frame.name];
|
|
65
|
-
const parent = stack.length > 0 ? stack[stack.length - 1].children : result;
|
|
66
|
-
if (component) parent.push(addKeyIfNeeded(component(frame.children), `rich-${keyIndex++}`));
|
|
67
|
-
else parent.push(...frame.children);
|
|
68
|
-
} else if (part.kind === "standalone") {
|
|
69
|
-
const component = components[part.name];
|
|
70
|
-
if (component) target.push(addKeyIfNeeded(component([]), `rich-${keyIndex++}`));
|
|
71
|
-
}
|
|
72
|
-
} else if (part.type === "text") target.push(part.value);
|
|
81
|
+
const target = stack.length > 0 ? stack.at(-1).children : result;
|
|
82
|
+
if (isMarkupPart(part)) applyMarkupPart(part, stack, result, components, nextKey);
|
|
83
|
+
else if (part.type === "text") target.push(part.value);
|
|
73
84
|
else if (part.type === "bidiIsolation") {} else if ("value" in part && part.value !== void 0 && part.value !== null) {
|
|
74
85
|
const stringValue = String(part.value);
|
|
75
86
|
target.push(stringValue);
|
|
@@ -77,11 +88,11 @@ function partsToNodes(parts, components) {
|
|
|
77
88
|
}
|
|
78
89
|
while (stack.length > 0) {
|
|
79
90
|
const frame = stack.pop();
|
|
80
|
-
(stack.length > 0 ? stack
|
|
91
|
+
(stack.length > 0 ? stack.at(-1).children : result).push(...frame.children);
|
|
81
92
|
}
|
|
82
93
|
return result;
|
|
83
94
|
}
|
|
84
95
|
//#endregion
|
|
85
96
|
export { normalizeMeta as n, partsToNodes as t };
|
|
86
97
|
|
|
87
|
-
//# sourceMappingURL=rich-
|
|
98
|
+
//# sourceMappingURL=rich-BDqjhv2h.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rich-BDqjhv2h.js","names":[],"sources":["../src/shared/meta.ts","../src/shared/rich.ts"],"sourcesContent":["import type { TranslationMeta } from './types.js';\n\n/**\n * Normalize meta argument to TranslationMeta object\n */\nexport function normalizeMeta(meta: TranslationMeta | string): TranslationMeta {\n return typeof meta === 'string' ? { fallback: meta } : meta;\n}\n","import type { MessageMarkupPart, MessagePart } from 'messageformat';\n\n// MessagePart<string> covers all built-in part types\ntype Part = MessagePart<string>;\n\n/**\n * Component callbacks for rich text rendering.\n * Each key maps to a function that wraps children in a component.\n *\n * @example\n * ```tsx\n * const components: RichComponents = {\n * link: (children) => <a href=\"/tos\">{children}</a>,\n * bold: (children) => <strong>{children}</strong>,\n * };\n * ```\n */\nexport type RichComponents<T = unknown> = Record<string, (children: (string | T)[]) => T>;\n\ntype StackFrame<T> = {\n name: string;\n children: (string | T)[];\n};\n\n// react element symbols - Symbol.for() returns the same symbol React uses internally\nconst REACT_ELEMENT_TYPE = Symbol.for('react.element');\nconst REACT_TRANSITIONAL_ELEMENT_TYPE = Symbol.for('react.transitional.element');\n\ntype ReactElementLike = {\n $$typeof: symbol;\n key: string | null;\n};\n\n/**\n * Duck-type check for React elements without importing React.\n * Uses Symbol.for() which returns the same symbol React uses internally.\n */\nfunction isReactElement(value: unknown): value is ReactElementLike {\n if (typeof value !== 'object' || value === null || !('$$typeof' in value)) {\n return false;\n }\n\n const type = (value as ReactElementLike).$$typeof;\n\n return type === REACT_ELEMENT_TYPE || type === REACT_TRANSITIONAL_ELEMENT_TYPE;\n}\n\n/**\n * Add a key to a React element if it doesn't have one.\n * Creates a shallow copy since React elements are frozen.\n */\nfunction addKeyIfNeeded<T>(element: T, key: string): T {\n if (isReactElement(element) && element.key === null) {\n return { ...element, key } as T;\n }\n\n return element;\n}\n\nfunction isMarkupPart(part: Part): part is MessageMarkupPart {\n return part.type === 'markup';\n}\n\nfunction applyMarkupPart<T>(\n part: MessageMarkupPart,\n stack: StackFrame<T>[],\n result: (string | T)[],\n components: RichComponents<T>,\n nextKey: () => string,\n): void {\n const target = stack.length > 0 ? stack.at(-1)!.children : result;\n\n switch (part.kind) {\n case 'open': {\n stack.push({ name: part.name, children: [] });\n\n break;\n }\n\n case 'close': {\n const frame = stack.pop();\n\n if (!frame) {\n // mismatched close tag, ignore\n break;\n }\n\n const component = components[frame.name];\n const parent = stack.length > 0 ? stack.at(-1)!.children : result;\n\n if (component) {\n parent.push(addKeyIfNeeded(component(frame.children), nextKey()));\n } else {\n // no component for this tag, just flatten children\n parent.push(...frame.children);\n }\n\n break;\n }\n\n case 'standalone': {\n // self-closing: {#icon/}\n const component = components[part.name];\n\n if (component) {\n target.push(addKeyIfNeeded(component([]), nextKey()));\n }\n\n break;\n }\n }\n}\n\n/**\n * Convert MF2 formatToParts() output to a tree of nodes.\n * Works with any JSX runtime (Astro, React, etc.) - the callbacks\n * return whatever JSX elements the active runtime compiles to.\n *\n * @example\n * MF2 message: \"Read our {#link}Terms{/link}\"\n * Parts: [\n * { type: 'text', value: 'Read our ' },\n * { type: 'markup', kind: 'open', name: 'link' },\n * { type: 'text', value: 'Terms' },\n * { type: 'markup', kind: 'close', name: 'link' },\n * ]\n * Result: ['Read our ', <a href=\"/tos\">Terms</a>]\n */\nexport function partsToNodes<T>(parts: Part[], components: RichComponents<T>): (string | T)[] {\n const result: (string | T)[] = [];\n const stack: StackFrame<T>[] = [];\n let keyIndex = 0;\n const nextKey = () => `rich-${keyIndex++}`;\n\n for (const part of parts) {\n const target = stack.length > 0 ? stack.at(-1)!.children : result;\n\n if (isMarkupPart(part)) {\n applyMarkupPart(part, stack, result, components, nextKey);\n } else if (part.type === 'text') {\n // plain text content - MessageTextPart.value is a string\n target.push(part.value as string);\n } else if (part.type === 'bidiIsolation') {\n // bidi isolation marks are direction control characters, not content\n } else {\n if ('value' in part && part.value !== undefined && part.value !== null) {\n const stringValue = String(part.value);\n\n target.push(stringValue);\n }\n }\n }\n\n // handle unclosed tags by flattening remaining stack frames\n while (stack.length > 0) {\n const frame = stack.pop()!;\n const parent = stack.length > 0 ? stack.at(-1)!.children : result;\n\n parent.push(...frame.children);\n }\n\n return result;\n}\n"],"mappings":";;;;AAKA,SAAgB,cAAc,MAAiD;CAC7E,OAAO,OAAO,SAAS,WAAW,EAAE,UAAU,KAAK,IAAI;AACzD;;;ACkBA,MAAM,qBAAqB,OAAO,IAAI,eAAe;AACrD,MAAM,kCAAkC,OAAO,IAAI,4BAA4B;;;;;AAW/E,SAAS,eAAe,OAA2C;CACjE,IAAI,OAAO,UAAU,YAAY,UAAU,QAAQ,EAAE,cAAc,QACjE,OAAO;CAGT,MAAM,OAAQ,MAA2B;CAEzC,OAAO,SAAS,sBAAsB,SAAS;AACjD;;;;;AAMA,SAAS,eAAkB,SAAY,KAAgB;CACrD,IAAI,eAAe,OAAO,KAAK,QAAQ,QAAQ,MAC7C,OAAO;EAAE,GAAG;EAAS;CAAI;CAG3B,OAAO;AACT;AAEA,SAAS,aAAa,MAAuC;CAC3D,OAAO,KAAK,SAAS;AACvB;AAEA,SAAS,gBACP,MACA,OACA,QACA,YACA,SACM;CACN,MAAM,SAAS,MAAM,SAAS,IAAI,MAAM,GAAG,EAAE,CAAC,CAAE,WAAW;CAE3D,QAAQ,KAAK,MAAb;EACE,KAAK;GACH,MAAM,KAAK;IAAE,MAAM,KAAK;IAAM,UAAU,CAAC;GAAE,CAAC;GAE5C;EAGF,KAAK,SAAS;GACZ,MAAM,QAAQ,MAAM,IAAI;GAExB,IAAI,CAAC,OAEH;GAGF,MAAM,YAAY,WAAW,MAAM;GACnC,MAAM,SAAS,MAAM,SAAS,IAAI,MAAM,GAAG,EAAE,CAAC,CAAE,WAAW;GAE3D,IAAI,WACF,OAAO,KAAK,eAAe,UAAU,MAAM,QAAQ,GAAG,QAAQ,CAAC,CAAC;QAGhE,OAAO,KAAK,GAAG,MAAM,QAAQ;GAG/B;EACF;EAEA,KAAK,cAAc;GAEjB,MAAM,YAAY,WAAW,KAAK;GAElC,IAAI,WACF,OAAO,KAAK,eAAe,UAAU,CAAC,CAAC,GAAG,QAAQ,CAAC,CAAC;GAGtD;EACF;CACF;AACF;;;;;;;;;;;;;;;;AAiBA,SAAgB,aAAgB,OAAe,YAA+C;CAC5F,MAAM,SAAyB,CAAC;CAChC,MAAM,QAAyB,CAAC;CAChC,IAAI,WAAW;CACf,MAAM,gBAAgB,QAAQ;CAE9B,KAAK,MAAM,QAAQ,OAAO;EACxB,MAAM,SAAS,MAAM,SAAS,IAAI,MAAM,GAAG,EAAE,CAAC,CAAE,WAAW;EAE3D,IAAI,aAAa,IAAI,GACnB,gBAAgB,MAAM,OAAO,QAAQ,YAAY,OAAO;OACnD,IAAI,KAAK,SAAS,QAEvB,OAAO,KAAK,KAAK,KAAe;OAC3B,IAAI,KAAK,SAAS,iBAAiB,CAE1C,OACE,IAAI,WAAW,QAAQ,KAAK,UAAU,KAAA,KAAa,KAAK,UAAU,MAAM;GACtE,MAAM,cAAc,OAAO,KAAK,KAAK;GAErC,OAAO,KAAK,WAAW;EACzB;CAEJ;CAGA,OAAO,MAAM,SAAS,GAAG;EACvB,MAAM,QAAQ,MAAM,IAAI;EAGxB,CAFe,MAAM,SAAS,IAAI,MAAM,GAAG,EAAE,CAAC,CAAE,WAAW,OAAA,CAEpD,KAAK,GAAG,MAAM,QAAQ;CAC/B;CAEA,OAAO;AACT"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"locale-middleware-entrypoint.d.ts","names":[],"sources":["../../src/server/locale-middleware-entrypoint.ts"],"mappings":";qBAGa,2BAAS"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { n as createI18nLocaleMiddleware } from "../middleware-BSEtDRYy.js";
|
|
2
|
+
import { exclude } from "virtual:@astroscope/i18n/config";
|
|
3
|
+
//#region src/server/locale-middleware-entrypoint.ts
|
|
4
|
+
const onRequest = createI18nLocaleMiddleware({ exclude: exclude ?? void 0 });
|
|
5
|
+
//#endregion
|
|
6
|
+
export { onRequest };
|
|
7
|
+
|
|
8
|
+
//# sourceMappingURL=locale-middleware-entrypoint.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"locale-middleware-entrypoint.js","names":[],"sources":["../../src/server/locale-middleware-entrypoint.ts"],"sourcesContent":["import { exclude } from 'virtual:@astroscope/i18n/config';\nimport { createI18nLocaleMiddleware } from './middleware.js';\n\nexport const onRequest = createI18nLocaleMiddleware({ exclude: exclude ?? undefined });\n"],"mappings":";;;AAGA,MAAa,YAAY,2BAA2B,EAAE,SAAS,WAAW,KAAA,EAAU,CAAC"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { t as RichComponents } from "../rich-BWszYAzS.js";
|
|
2
|
-
import { a as TranslationMeta, i as TranslateFunction } from "../types-
|
|
2
|
+
import { a as TranslationMeta, i as TranslateFunction } from "../types-BjTD5hUT.js";
|
|
3
3
|
//#region src/server/translate.d.ts
|
|
4
4
|
/**
|
|
5
5
|
* Get the current locale from request context.
|
|
@@ -23,7 +23,7 @@ export declare const t: TranslateFunction;
|
|
|
23
23
|
* @param key - Translation key
|
|
24
24
|
* @param meta - Fallback string or meta object with fallback (used as message template)
|
|
25
25
|
* @param components - Map of tag names to component callbacks
|
|
26
|
-
* @param values - Runtime interpolation values (
|
|
26
|
+
* @param values - Runtime interpolation values (MessageFormat 2)
|
|
27
27
|
*
|
|
28
28
|
* @example
|
|
29
29
|
* ```tsx
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"translate.d.ts","names":[],"sources":["../../src/server/translate.ts"],"mappings":";;;;;;;
|
|
1
|
+
{"version":3,"file":"translate.d.ts","names":[],"sources":["../../src/server/translate.ts"],"mappings":";;;;;;;wBAqDgB;;;;;;;;qBAYH,GAAG;;;;;;;;;;;;;;;;;;;;wBA2DA,KAAK,aACnB,aACA,OAAO,sCACP,aAAa,eAAe,gBAC5B,SAAS,gDACE"}
|
package/dist/server/translate.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { t as i18n } from "../i18n-C47eQi8U.js";
|
|
2
2
|
import { r as formatMessageToParts, t as compileMessage } from "../compiler-CkOVGWU5.js";
|
|
3
|
-
import {
|
|
3
|
+
import { t as getContext } from "../context-BCUJxb8p.js";
|
|
4
|
+
import { n as normalizeMeta, t as partsToNodes } from "../rich-BDqjhv2h.js";
|
|
4
5
|
import { createCounter } from "@astroscope/node/telemetry";
|
|
5
6
|
import { log } from "@astroscope/node/log";
|
|
6
7
|
//#region src/server/translate.ts
|
|
@@ -76,7 +77,7 @@ const t = ((key, meta, values) => {
|
|
|
76
77
|
* @param key - Translation key
|
|
77
78
|
* @param meta - Fallback string or meta object with fallback (used as message template)
|
|
78
79
|
* @param components - Map of tag names to component callbacks
|
|
79
|
-
* @param values - Runtime interpolation values (
|
|
80
|
+
* @param values - Runtime interpolation values (MessageFormat 2)
|
|
80
81
|
*
|
|
81
82
|
* @example
|
|
82
83
|
* ```tsx
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"translate.js","names":[],"sources":["../../src/server/translate.ts"],"sourcesContent":["import { log } from '@astroscope/node/log';\nimport { createCounter } from '@astroscope/node/telemetry';\nimport { compileMessage, formatMessageToParts } from '../shared/compiler.js';\nimport { normalizeMeta } from '../shared/meta.js';\nimport { type RichComponents, partsToNodes } from '../shared/rich.js';\nimport type { CompiledTranslation, TranslateFunction, TranslationMeta } from '../shared/types.js';\nimport { getContext } from './context.js';\nimport { i18n } from './i18n.js';\nimport type { FallbackBehavior } from './types.js';\n\n// cache for fallbacks compiled outside request context (e.g. during build)\nconst fallbackCache = new Map<string, CompiledTranslation>();\n\nconst missing = createCounter('astro.i18n.missing', {\n scope: '@astroscope/i18n',\n description: 'Translation lookups that fell back (a key once per locale and translations version for t())',\n unit: '{lookup}',\n});\n\n// the key stays out of the metric label (unbounded) and goes to the log instead\nfunction reportMissing(key: string, locale: string): void {\n missing.add(1, { 'astro.i18n.locale': locale });\n log.debug({ key, locale }, 'missing translation');\n}\n\n/**\n * Apply fallback behavior when translation is missing\n */\nfunction applyFallback(key: string, meta: TranslationMeta, fallback: FallbackBehavior): string {\n if (typeof fallback === 'function') {\n return fallback(key, meta);\n }\n\n switch (fallback) {\n case 'key'
|
|
1
|
+
{"version":3,"file":"translate.js","names":[],"sources":["../../src/server/translate.ts"],"sourcesContent":["import { log } from '@astroscope/node/log';\nimport { createCounter } from '@astroscope/node/telemetry';\nimport { compileMessage, formatMessageToParts } from '../shared/compiler.js';\nimport { normalizeMeta } from '../shared/meta.js';\nimport { type RichComponents, partsToNodes } from '../shared/rich.js';\nimport type { CompiledTranslation, TranslateFunction, TranslationMeta } from '../shared/types.js';\nimport { getContext } from './context.js';\nimport { i18n } from './i18n.js';\nimport type { FallbackBehavior } from './types.js';\n\n// cache for fallbacks compiled outside request context (e.g. during build)\nconst fallbackCache = new Map<string, CompiledTranslation>();\n\nconst missing = createCounter('astro.i18n.missing', {\n scope: '@astroscope/i18n',\n description: 'Translation lookups that fell back (a key once per locale and translations version for t())',\n unit: '{lookup}',\n});\n\n// the key stays out of the metric label (unbounded) and goes to the log instead\nfunction reportMissing(key: string, locale: string): void {\n missing.add(1, { 'astro.i18n.locale': locale });\n log.debug({ key, locale }, 'missing translation');\n}\n\n/**\n * Apply fallback behavior when translation is missing\n */\nfunction applyFallback(key: string, meta: TranslationMeta, fallback: FallbackBehavior): string {\n if (typeof fallback === 'function') {\n return fallback(key, meta);\n }\n\n switch (fallback) {\n case 'key': {\n return key;\n }\n case 'fallback': {\n return meta.fallback || key;\n }\n case 'throw': {\n throw new Error(`Missing translation for key: ${key}`);\n }\n default: {\n return meta.fallback || key;\n }\n }\n}\n\n/**\n * Get the current locale from request context.\n * Returns defaultLocale if called outside a request (e.g. during build).\n */\nexport function getLocale(): string {\n const ctx = getContext();\n return ctx?.locale ?? i18n.getConfig().defaultLocale;\n}\n\n/**\n * Server-side translate function\n *\n * Call patterns:\n * - Development: t('key', 'fallback') or t('key', 'fallback', values)\n * - Production: t('key') or t('key', undefined, values)\n */\nexport const t: TranslateFunction = ((\n key: string,\n meta?: TranslationMeta | string,\n values?: Record<string, unknown>,\n): string => {\n const normalizedMeta: TranslationMeta = meta ? normalizeMeta(meta) : { fallback: '' };\n\n const ctx = getContext();\n\n if (!ctx) {\n const defaultLocale = i18n.getConfig().defaultLocale;\n const fallbackStr = normalizedMeta.fallback || key;\n const cacheKey = `${defaultLocale}:${fallbackStr}`;\n let compiled = fallbackCache.get(cacheKey);\n\n if (!compiled) {\n compiled = compileMessage(defaultLocale, fallbackStr);\n fallbackCache.set(cacheKey, compiled);\n }\n\n return compiled(values);\n }\n\n const compiled = ctx.translations[key];\n\n if (!compiled) {\n reportMissing(key, ctx.locale);\n\n const fallbackValue = applyFallback(key, normalizedMeta, ctx.fallback);\n\n const compiledFallback = compileMessage(ctx.locale, fallbackValue);\n\n ctx.translations[key] = compiledFallback;\n\n return compiledFallback(values);\n }\n\n return compiled(values);\n}) as TranslateFunction;\n\n/**\n * Server-side rich text translation function\n *\n * Parses MF2 markup syntax and applies component callbacks to wrap content.\n * Works with any JSX runtime (Astro, React during SSR, etc.).\n *\n * @param key - Translation key\n * @param meta - Fallback string or meta object with fallback (used as message template)\n * @param components - Map of tag names to component callbacks\n * @param values - Runtime interpolation values (MessageFormat 2)\n *\n * @example\n * ```tsx\n * rich('tos', 'Read our {#link}Terms{/link}', {\n * link: (children) => <a href=\"/tos\">{children}</a>\n * })\n * // Returns: ['Read our ', <a href=\"/tos\">Terms</a>]\n * ```\n */\nexport function rich<T = unknown>(\n key: string,\n meta?: TranslationMeta | string | undefined,\n components?: RichComponents<T> | undefined,\n values?: Record<string, unknown> | undefined,\n): (string | T)[] {\n const normalizedMeta: TranslationMeta = meta ? normalizeMeta(meta) : { fallback: '' };\n\n const ctx = getContext();\n const locale = ctx?.locale ?? i18n.getConfig().defaultLocale;\n\n const translated = ctx?.rawTranslations[key];\n\n if (ctx && !translated) {\n reportMissing(key, ctx.locale);\n }\n\n const raw = translated || normalizedMeta.fallback || key;\n\n const parts = formatMessageToParts(locale, raw, values);\n\n return partsToNodes(parts, components ?? {});\n}\n"],"mappings":";;;;;;;AAWA,MAAM,gCAAgB,IAAI,IAAiC;AAE3D,MAAM,UAAU,cAAc,sBAAsB;CAClD,OAAO;CACP,aAAa;CACb,MAAM;AACR,CAAC;AAGD,SAAS,cAAc,KAAa,QAAsB;CACxD,QAAQ,IAAI,GAAG,EAAE,qBAAqB,OAAO,CAAC;CAC9C,IAAI,MAAM;EAAE;EAAK;CAAO,GAAG,qBAAqB;AAClD;;;;AAKA,SAAS,cAAc,KAAa,MAAuB,UAAoC;CAC7F,IAAI,OAAO,aAAa,YACtB,OAAO,SAAS,KAAK,IAAI;CAG3B,QAAQ,UAAR;EACE,KAAK,OACH,OAAO;EAET,KAAK,YACH,OAAO,KAAK,YAAY;EAE1B,KAAK,SACH,MAAM,IAAI,MAAM,gCAAgC,KAAK;EAEvD,SACE,OAAO,KAAK,YAAY;CAE5B;AACF;;;;;AAMA,SAAgB,YAAoB;CAElC,OADY,WACH,CAAC,EAAE,UAAU,KAAK,UAAU,CAAC,CAAC;AACzC;;;;;;;;AASA,MAAa,MACX,KACA,MACA,WACW;CACX,MAAM,iBAAkC,OAAO,cAAc,IAAI,IAAI,EAAE,UAAU,GAAG;CAEpF,MAAM,MAAM,WAAW;CAEvB,IAAI,CAAC,KAAK;EACR,MAAM,gBAAgB,KAAK,UAAU,CAAC,CAAC;EACvC,MAAM,cAAc,eAAe,YAAY;EAC/C,MAAM,WAAW,GAAG,cAAc,GAAG;EACrC,IAAI,WAAW,cAAc,IAAI,QAAQ;EAEzC,IAAI,CAAC,UAAU;GACb,WAAW,eAAe,eAAe,WAAW;GACpD,cAAc,IAAI,UAAU,QAAQ;EACtC;EAEA,OAAO,SAAS,MAAM;CACxB;CAEA,MAAM,WAAW,IAAI,aAAa;CAElC,IAAI,CAAC,UAAU;EACb,cAAc,KAAK,IAAI,MAAM;EAE7B,MAAM,gBAAgB,cAAc,KAAK,gBAAgB,IAAI,QAAQ;EAErE,MAAM,mBAAmB,eAAe,IAAI,QAAQ,aAAa;EAEjE,IAAI,aAAa,OAAO;EAExB,OAAO,iBAAiB,MAAM;CAChC;CAEA,OAAO,SAAS,MAAM;AACxB;;;;;;;;;;;;;;;;;;;;AAqBA,SAAgB,KACd,KACA,MACA,YACA,QACgB;CAChB,MAAM,iBAAkC,OAAO,cAAc,IAAI,IAAI,EAAE,UAAU,GAAG;CAEpF,MAAM,MAAM,WAAW;CACvB,MAAM,SAAS,KAAK,UAAU,KAAK,UAAU,CAAC,CAAC;CAE/C,MAAM,aAAa,KAAK,gBAAgB;CAExC,IAAI,OAAO,CAAC,YACV,cAAc,KAAK,IAAI,MAAM;CAG/B,MAAM,MAAM,cAAc,eAAe,YAAY;CAErD,MAAM,QAAQ,qBAAqB,QAAQ,KAAK,MAAM;CAEtD,OAAO,aAAa,OAAO,cAAc,CAAC,CAAC;AAC7C"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as TranslationMeta } from "./types-
|
|
1
|
+
import { a as TranslationMeta } from "./types-BjTD5hUT.js";
|
|
2
2
|
//#region src/extraction/types.d.ts
|
|
3
3
|
/**
|
|
4
4
|
* Deduplicated translation key with all file locations (used in manifest)
|
|
@@ -28,4 +28,4 @@ type ExtractionManifest = {
|
|
|
28
28
|
};
|
|
29
29
|
//#endregion
|
|
30
30
|
export { ExtractedKey as n, ExtractionManifest as r, ChunkManifest as t };
|
|
31
|
-
//# sourceMappingURL=types-
|
|
31
|
+
//# sourceMappingURL=types-BC-tQ30r.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types-
|
|
1
|
+
{"version":3,"file":"types-BC-tQ30r.d.ts","names":[],"sources":["../src/extraction/types.ts"],"mappings":";;;;;KA8BY;EACV;EACA,MAAM;;EAEN;;;;;KAMU,gBAAgB;;;;KAKhB;EACV,MAAM;EACN,QAAQ;;;;;;EAMR"}
|
|
@@ -41,14 +41,16 @@ type I18nClientState = {
|
|
|
41
41
|
*
|
|
42
42
|
* @param key - translation key
|
|
43
43
|
* @param meta - fallback string or meta object with fallback
|
|
44
|
-
* @param values - runtime interpolation values (
|
|
44
|
+
* @param values - runtime interpolation values (MessageFormat 2)
|
|
45
45
|
*
|
|
46
46
|
* @example
|
|
47
47
|
* t('checkout.title', 'Order Summary')
|
|
48
|
-
* t('cart.items', '{count
|
|
49
|
-
*
|
|
48
|
+
* t('cart.items', '.input {$count :number}\n.match $count\none {{{$count} item}}\n* {{{$count} items}}', {
|
|
49
|
+
* count: itemCount,
|
|
50
|
+
* })
|
|
51
|
+
* t('cart.total', { fallback: 'Total: {$amount}' }, { amount: '$49.99' })
|
|
50
52
|
*/
|
|
51
53
|
type TranslateFunction = (key: string, meta: TranslationMeta | string, values?: Record<string, unknown> | undefined) => string;
|
|
52
54
|
//#endregion
|
|
53
55
|
export { TranslationMeta as a, TranslateFunction as i, I18nClientState as n, RawTranslations as r, CompiledTranslations as t };
|
|
54
|
-
//# sourceMappingURL=types-
|
|
56
|
+
//# sourceMappingURL=types-BjTD5hUT.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types-
|
|
1
|
+
{"version":3,"file":"types-BjTD5hUT.d.ts","names":[],"sources":["../src/shared/types.ts"],"mappings":";;;;KAGY;EACV;EACA;;;;;KAMU;EACV;EACA,YAAY,eAAe;EAC3B;;;;;KAMU,uBAAuB,SAAS;;;;KAKhC,uBAAuB,eAAe;;;;KAKtC,kBAAkB;;;;;;KAOlB;EACV;EACA,QAAQ;EACR,cAAc;;;;;;;;;;;;;;;;KAiBJ,qBACV,aACA,MAAM,0BACN,SAAS"}
|