@astroscope/i18n 1.1.2 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +28 -72
- package/dist/babel-plugin-CWafmw5z.js.map +1 -1
- package/dist/client/translate.d.ts +1 -1
- package/dist/client/translate.d.ts.map +1 -1
- package/dist/client/translate.js +19 -2
- package/dist/client/translate.js.map +1 -1
- package/dist/{context-BEQ6qqX5.js → context-CYqyZJyH.js} +14 -66
- package/dist/context-CYqyZJyH.js.map +1 -0
- package/dist/extraction/index.d.ts +1 -1
- package/dist/extraction/index.d.ts.map +1 -1
- package/dist/extraction/index.js +2 -2
- package/dist/{i18n-Dzh54-at.d.ts → i18n-Bp8TFmd3.d.ts} +7 -8
- package/dist/i18n-Bp8TFmd3.d.ts.map +1 -0
- package/dist/index.d.ts +14 -30
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +20 -152
- package/dist/index.js.map +1 -1
- package/dist/internal.d.ts +2 -2
- package/dist/internal.js +1 -1
- package/dist/{manifest-BEm6GezL.js → manifest-Cma68XM-.js} +3 -3
- package/dist/manifest-Cma68XM-.js.map +1 -0
- package/dist/middleware-DxKY84Iv.js +235 -0
- package/dist/middleware-DxKY84Iv.js.map +1 -0
- package/dist/rich-CHisXE-A.js.map +1 -1
- package/dist/server/chunk-middleware-entrypoint.d.ts +5 -0
- package/dist/server/chunk-middleware-entrypoint.d.ts.map +1 -0
- package/dist/server/chunk-middleware-entrypoint.js +7 -0
- package/dist/server/chunk-middleware-entrypoint.js.map +1 -0
- package/dist/server/translate.d.ts +1 -1
- package/dist/server/translate.d.ts.map +1 -1
- package/dist/server/translate.js +5 -2
- package/dist/server/translate.js.map +1 -1
- package/dist/{types-C98CcjtN.d.ts → types-DU8QHch4.d.ts} +4 -3
- package/dist/types-DU8QHch4.d.ts.map +1 -0
- package/dist/{types-CkqZWnXH.d.ts → types-f-SNJdvf.d.ts} +8 -8
- package/dist/types-f-SNJdvf.d.ts.map +1 -0
- package/dist/{url-B8334XSD.js → url-CifeaDzl.js} +2 -12
- package/dist/{url-B8334XSD.js.map → url-CifeaDzl.js.map} +1 -1
- package/dist/{vite-plugin-BbdVKdt8.js → vite-plugin-C-jEpd8t.js} +18 -22
- package/dist/vite-plugin-C-jEpd8t.js.map +1 -0
- package/package.json +14 -35
- package/dist/client/directives/idle.d.ts +0 -6
- package/dist/client/directives/idle.d.ts.map +0 -1
- package/dist/client/directives/idle.js +0 -14
- package/dist/client/directives/idle.js.map +0 -1
- package/dist/client/directives/load.d.ts +0 -6
- package/dist/client/directives/load.d.ts.map +0 -1
- package/dist/client/directives/load.js +0 -10
- package/dist/client/directives/load.js.map +0 -1
- package/dist/client/directives/media.d.ts +0 -6
- package/dist/client/directives/media.d.ts.map +0 -1
- package/dist/client/directives/media.js +0 -17
- package/dist/client/directives/media.js.map +0 -1
- package/dist/client/directives/only.d.ts +0 -6
- package/dist/client/directives/only.d.ts.map +0 -1
- package/dist/client/directives/only.js +0 -10
- package/dist/client/directives/only.js.map +0 -1
- package/dist/client/directives/utils.d.ts +0 -10
- package/dist/client/directives/utils.d.ts.map +0 -1
- package/dist/client/directives/utils.js +0 -32
- package/dist/client/directives/utils.js.map +0 -1
- package/dist/client/directives/visible.d.ts +0 -6
- package/dist/client/directives/visible.d.ts.map +0 -1
- package/dist/client/directives/visible.js +0 -23
- package/dist/client/directives/visible.js.map +0 -1
- package/dist/context-BEQ6qqX5.js.map +0 -1
- package/dist/i18n-Dzh54-at.d.ts.map +0 -1
- package/dist/manifest-BEm6GezL.js.map +0 -1
- package/dist/state-DeudhdeT.js +0 -20
- package/dist/state-DeudhdeT.js.map +0 -1
- package/dist/types-C98CcjtN.d.ts.map +0 -1
- package/dist/types-CkqZWnXH.d.ts.map +0 -1
- package/dist/vite-plugin-BbdVKdt8.js.map +0 -1
- package/src/astro/I18nScript.astro +0 -16
- package/src/astro/index.ts +0 -2
package/dist/index.js
CHANGED
|
@@ -1,163 +1,31 @@
|
|
|
1
|
-
import { t as i18nVitePlugin } from "./vite-plugin-
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { overrideRequestRoute } from "@astroscope/node/log";
|
|
1
|
+
import { t as i18nVitePlugin } from "./vite-plugin-C-jEpd8t.js";
|
|
2
|
+
import { r as i18n } from "./context-CYqyZJyH.js";
|
|
3
|
+
import { n as createI18nMiddleware } from "./middleware-DxKY84Iv.js";
|
|
5
4
|
//#region src/integration/integration.ts
|
|
6
|
-
const CLIENT_DIRECTIVES = [
|
|
7
|
-
"load-x",
|
|
8
|
-
"idle-x",
|
|
9
|
-
"visible-x",
|
|
10
|
-
"media-x",
|
|
11
|
-
"only-x"
|
|
12
|
-
];
|
|
13
5
|
/**
|
|
14
6
|
* Astro integration for i18n with automatic per-chunk translation loading.
|
|
15
7
|
*
|
|
16
|
-
* Registers
|
|
17
|
-
*
|
|
18
|
-
*
|
|
8
|
+
* Registers the vite plugin for translation key extraction, chunk mapping and
|
|
9
|
+
* loader injection, and injects the chunk middleware serving `/_i18n/`
|
|
10
|
+
* translation chunks. Client state delivery and translation preloading happen at
|
|
11
|
+
* runtime: the i18n middleware bootstraps `window.__i18n__`, and the islands
|
|
12
|
+
* emitter (via `@astroscope/node`) slices hashes and preloads translation chunks
|
|
13
|
+
* per island — plain `client:*` directives, no forks.
|
|
19
14
|
*/
|
|
20
15
|
function i18nIntegration(options = {}) {
|
|
21
16
|
const { consistency = "error" } = options;
|
|
22
17
|
return {
|
|
23
18
|
name: "@astroscope/i18n",
|
|
24
|
-
hooks: {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
},
|
|
35
|
-
"astro:config:done": ({ injectTypes }) => {
|
|
36
|
-
const typeMap = {
|
|
37
|
-
load: "boolean",
|
|
38
|
-
idle: "boolean",
|
|
39
|
-
visible: "boolean | string",
|
|
40
|
-
media: "string",
|
|
41
|
-
only: "string"
|
|
42
|
-
};
|
|
43
|
-
injectTypes({
|
|
44
|
-
filename: "client-directives.d.ts",
|
|
45
|
-
content: `// generated by @astroscope/i18n
|
|
46
|
-
declare namespace astroHTML.JSX {
|
|
47
|
-
interface AstroBuiltinAttributes {
|
|
48
|
-
${CLIENT_DIRECTIVES.map((d) => {
|
|
49
|
-
const base = d.replace("-x", "");
|
|
50
|
-
return ` 'client:${d}'?: ${typeMap[base]};`;
|
|
51
|
-
}).join("\n")}
|
|
52
|
-
}
|
|
53
|
-
}`
|
|
54
|
-
});
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
};
|
|
58
|
-
}
|
|
59
|
-
//#endregion
|
|
60
|
-
//#region src/server/middleware.ts
|
|
61
|
-
const I18N_ENDPOINT_PREFIX = "/_i18n/";
|
|
62
|
-
const I18N_ENDPOINT_ROUTE = "/_i18n/[locale]/[chunk]";
|
|
63
|
-
/**
|
|
64
|
-
* Serve a translation chunk. Returns `undefined` when the path isn't a chunk
|
|
65
|
-
* request, and the caller passes it on to astro.
|
|
66
|
-
*/
|
|
67
|
-
function createChunkResponse(pathname) {
|
|
68
|
-
if (!pathname.startsWith(I18N_ENDPOINT_PREFIX)) return;
|
|
69
|
-
if (!i18n.isConfigured()) {
|
|
70
|
-
console.warn(`[@astroscope/i18n] not configured, passing through: ${pathname}`);
|
|
71
|
-
return;
|
|
72
|
-
}
|
|
73
|
-
const path = pathname.slice(7);
|
|
74
|
-
const slashIdx = path.indexOf("/");
|
|
75
|
-
if (slashIdx === -1) return;
|
|
76
|
-
const locale = path.slice(0, slashIdx);
|
|
77
|
-
if (!i18n.getConfig().locales.includes(locale)) return new Response("/* unknown locale */", {
|
|
78
|
-
status: 404,
|
|
79
|
-
headers: { "Content-Type": "application/javascript" }
|
|
80
|
-
});
|
|
81
|
-
const rest = path.slice(slashIdx + 1);
|
|
82
|
-
if (!rest.endsWith(".js")) return;
|
|
83
|
-
const withoutJs = rest.slice(0, -3);
|
|
84
|
-
const lastDotIdx = withoutJs.lastIndexOf(".");
|
|
85
|
-
if (lastDotIdx === -1) return;
|
|
86
|
-
const chunkName = withoutJs.slice(0, lastDotIdx);
|
|
87
|
-
const body = i18n.getChunkBody(locale, chunkName);
|
|
88
|
-
if (!body) return new Response(`/* chunk not found: ${chunkName} */`, {
|
|
89
|
-
status: 404,
|
|
90
|
-
headers: { "Content-Type": "application/javascript" }
|
|
91
|
-
});
|
|
92
|
-
return new Response(body, {
|
|
93
|
-
status: 200,
|
|
94
|
-
headers: {
|
|
95
|
-
"Content-Type": "text/javascript; charset=utf-8",
|
|
96
|
-
"Content-Length": String(body.byteLength),
|
|
97
|
-
"Cache-Control": "public, max-age=31536000, immutable"
|
|
98
|
-
}
|
|
99
|
-
});
|
|
100
|
-
}
|
|
101
|
-
/**
|
|
102
|
-
* Create the i18n chunk middleware that serves translation chunks at `/_i18n/` endpoints.
|
|
103
|
-
*
|
|
104
|
-
* Place this early in your middleware sequence (before session/auth)
|
|
105
|
-
* to avoid unnecessary overhead for static translation requests.
|
|
106
|
-
*
|
|
107
|
-
* @example
|
|
108
|
-
* ```typescript
|
|
109
|
-
* // src/middleware.ts
|
|
110
|
-
* import { sequence } from 'astro:middleware';
|
|
111
|
-
* import { createI18nChunkMiddleware, createI18nMiddleware } from '@astroscope/i18n';
|
|
112
|
-
*
|
|
113
|
-
* export const onRequest = sequence(
|
|
114
|
-
* createI18nChunkMiddleware(), // early: serves /_i18n/ chunks
|
|
115
|
-
* sessionMiddleware,
|
|
116
|
-
* createI18nMiddleware({ locale: (ctx) => ... }), // after session
|
|
117
|
-
* );
|
|
118
|
-
* ```
|
|
119
|
-
*/
|
|
120
|
-
function createI18nChunkMiddleware() {
|
|
121
|
-
return (ctx, next) => {
|
|
122
|
-
const response = createChunkResponse(ctx.url.pathname);
|
|
123
|
-
if (!response) return next();
|
|
124
|
-
overrideRequestRoute(I18N_ENDPOINT_ROUTE);
|
|
125
|
-
return response;
|
|
126
|
-
};
|
|
127
|
-
}
|
|
128
|
-
/**
|
|
129
|
-
* Create the i18n locale middleware.
|
|
130
|
-
*
|
|
131
|
-
* Sets up the request context with locale and translations for use by
|
|
132
|
-
* `t()` and `<I18nScript />`. Place this after session middleware if
|
|
133
|
-
* your locale detection depends on session/cookies.
|
|
134
|
-
*
|
|
135
|
-
* @example
|
|
136
|
-
* ```typescript
|
|
137
|
-
* import { createI18nMiddleware, detectLocale, i18n } from '@astroscope/i18n';
|
|
138
|
-
*
|
|
139
|
-
* export const i18nMiddleware = createI18nMiddleware({
|
|
140
|
-
* locale: (ctx) =>
|
|
141
|
-
* ctx.cookies.get('locale')?.value ??
|
|
142
|
-
* detectLocale(ctx.request) ??
|
|
143
|
-
* i18n.getConfig().defaultLocale,
|
|
144
|
-
* });
|
|
145
|
-
* ```
|
|
146
|
-
*/
|
|
147
|
-
function createI18nMiddleware(options) {
|
|
148
|
-
return (ctx, next) => {
|
|
149
|
-
if (shouldExclude(ctx, options.exclude ?? RECOMMENDED_EXCLUDES)) return next();
|
|
150
|
-
if (!i18n.isConfigured()) {
|
|
151
|
-
console.warn(`[@astroscope/i18n] not configured, passing through: ${ctx.url.pathname}`);
|
|
152
|
-
return next();
|
|
153
|
-
}
|
|
154
|
-
const locale = options.locale(ctx);
|
|
155
|
-
return runWithContext({
|
|
156
|
-
locale,
|
|
157
|
-
translations: i18n.getCompiledTranslations(locale),
|
|
158
|
-
rawTranslations: i18n.getTranslations(locale),
|
|
159
|
-
fallback: i18n.getConfig().fallback
|
|
160
|
-
}, () => next());
|
|
19
|
+
hooks: { "astro:config:setup": ({ addMiddleware, updateConfig, logger }) => {
|
|
20
|
+
addMiddleware({
|
|
21
|
+
order: "pre",
|
|
22
|
+
entrypoint: "@astroscope/i18n/chunk-middleware"
|
|
23
|
+
});
|
|
24
|
+
updateConfig({ vite: { plugins: [i18nVitePlugin({
|
|
25
|
+
logger,
|
|
26
|
+
consistency
|
|
27
|
+
})] } });
|
|
28
|
+
} }
|
|
161
29
|
};
|
|
162
30
|
}
|
|
163
31
|
//#endregion
|
|
@@ -181,6 +49,6 @@ function detectLocale(request) {
|
|
|
181
49
|
for (const { lang } of preferred) if (locales.includes(lang)) return lang;
|
|
182
50
|
}
|
|
183
51
|
//#endregion
|
|
184
|
-
export {
|
|
52
|
+
export { createI18nMiddleware, i18nIntegration as default, detectLocale, i18n };
|
|
185
53
|
|
|
186
54
|
//# 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/middleware.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\nconst CLIENT_DIRECTIVES = ['load-x', 'idle-x', 'visible-x', 'media-x', 'only-x'] as const;\n\n/**\n * Astro integration for i18n with automatic per-chunk translation loading.\n *\n * Registers:\n * - Client directives (`client:load-x`, `client:idle-x`, `client:visible-x`, `client:media-x`, `client:only-x`)\n * - Vite plugin for translation key extraction\n */\nexport default function i18nIntegration(options: I18nOptions = {}): AstroIntegration {\n const { consistency = 'error' } = options;\n\n return {\n name: '@astroscope/i18n',\n hooks: {\n 'astro:config:setup': ({ addClientDirective, updateConfig, logger }) => {\n for (const directive of CLIENT_DIRECTIVES) {\n const base = directive.replace('-x', '');\n addClientDirective({\n name: directive,\n entrypoint: `@astroscope/i18n/client-directives/${base}.js`,\n });\n }\n\n updateConfig({\n vite: {\n plugins: [i18nVitePlugin({ logger, consistency })],\n },\n });\n },\n 'astro:config:done': ({ injectTypes }) => {\n // inject client directive types for LSP support\n const typeMap: Record<string, string> = {\n load: 'boolean',\n idle: 'boolean',\n visible: 'boolean | string',\n media: 'string',\n only: 'string',\n };\n\n const attributes = CLIENT_DIRECTIVES.map((d) => {\n const base = d.replace('-x', '');\n return ` 'client:${d}'?: ${typeMap[base]};`;\n }).join('\\n');\n\n injectTypes({\n filename: 'client-directives.d.ts',\n content: `// generated by @astroscope/i18n\ndeclare namespace astroHTML.JSX {\n interface AstroBuiltinAttributes {\n${attributes}\n }\n}`,\n });\n },\n },\n };\n}\n","import { type ExcludePattern, RECOMMENDED_EXCLUDES, shouldExclude } from '@astroscope/node/excludes';\nimport { overrideRequestRoute } from '@astroscope/node/log';\nimport type { APIContext, MiddlewareHandler } from 'astro';\nimport { runWithContext } from './context.js';\nimport { i18n } from './i18n.js';\nimport type { I18nContext } from './types.js';\n\nexport type I18nMiddlewareOptions = {\n locale: (ctx: APIContext) => string;\n /**\n * Patterns to exclude from locale context setup.\n * Defaults to RECOMMENDED_EXCLUDES if not provided.\n *\n * @example\n * ```ts\n * import { RECOMMENDED_EXCLUDES } from '@astroscope/node/excludes';\n *\n * // extend defaults\n * createI18nMiddleware({\n * locale: (ctx) => ctx.locals.session?.locale ?? 'en',\n * exclude: [...RECOMMENDED_EXCLUDES, { exact: '/health' }],\n * })\n *\n * // disable excludes entirely\n * createI18nMiddleware({\n * locale: (ctx) => ctx.locals.session?.locale ?? 'en',\n * exclude: [],\n * })\n * ```\n */\n exclude?: ExcludePattern[] | ((context: APIContext) => boolean) | 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 console.warn(`[@astroscope/i18n] not configured, passing through: ${pathname}`);\n\n return undefined;\n }\n\n const path = pathname.slice(I18N_ENDPOINT_PREFIX.length);\n\n // attempt to parse as efficient as possible\n // expected path format: {locale}/{chunkName}.{hash}.js\n // avoiding regex or split for performance\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 * Place this early in your middleware sequence (before session/auth)\n * to avoid unnecessary overhead for static translation requests.\n *\n * @example\n * ```typescript\n * // src/middleware.ts\n * import { sequence } from 'astro:middleware';\n * import { createI18nChunkMiddleware, createI18nMiddleware } from '@astroscope/i18n';\n *\n * export const onRequest = sequence(\n * createI18nChunkMiddleware(), // early: serves /_i18n/ chunks\n * sessionMiddleware,\n * createI18nMiddleware({ locale: (ctx) => ... }), // after session\n * );\n * ```\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 * Sets up the request context with locale and translations for use by\n * `t()` and `<I18nScript />`. Place this after session middleware if\n * your locale detection depends on session/cookies.\n *\n * @example\n * ```typescript\n * import { createI18nMiddleware, detectLocale, i18n } from '@astroscope/i18n';\n *\n * export const i18nMiddleware = createI18nMiddleware({\n * locale: (ctx) =>\n * ctx.cookies.get('locale')?.value ??\n * detectLocale(ctx.request) ??\n * i18n.getConfig().defaultLocale,\n * });\n * ```\n */\nexport function createI18nMiddleware(options: I18nMiddlewareOptions): MiddlewareHandler {\n return (ctx, next) => {\n if (shouldExclude(ctx, options.exclude ?? RECOMMENDED_EXCLUDES)) {\n return next();\n }\n\n if (!i18n.isConfigured()) {\n console.warn(`[@astroscope/i18n] not configured, passing through: ${ctx.url.pathname}`);\n\n return next();\n }\n\n const locale = options.locale(ctx);\n\n const context: I18nContext = {\n locale,\n translations: i18n.getCompiledTranslations(locale),\n rawTranslations: i18n.getTranslations(locale),\n fallback: i18n.getConfig().fallback,\n };\n\n return runWithContext(context, () => next());\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('-')[0]?.toLowerCase();\n\n return {\n lang: baseLang,\n q: parseFloat(q.replace('q=', '')),\n };\n })\n .filter((p): p is { lang: string; q: number } => !!p.lang)\n .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":";;;;;AAIA,MAAM,oBAAoB;CAAC;CAAU;CAAU;CAAa;CAAW;AAAQ;;;;;;;;AAS/E,SAAwB,gBAAgB,UAAuB,CAAC,GAAqB;CACnF,MAAM,EAAE,cAAc,YAAY;CAElC,OAAO;EACL,MAAM;EACN,OAAO;GACL,uBAAuB,EAAE,oBAAoB,cAAc,aAAa;IACtE,KAAK,MAAM,aAAa,mBAEtB,mBAAmB;KACjB,MAAM;KACN,YAAY,sCAHD,UAAU,QAAQ,MAAM,EAGkB,EAAE;IACzD,CAAC;IAGH,aAAa,EACX,MAAM,EACJ,SAAS,CAAC,eAAe;KAAE;KAAQ;IAAY,CAAC,CAAC,EACnD,EACF,CAAC;GACH;GACA,sBAAsB,EAAE,kBAAkB;IAExC,MAAM,UAAkC;KACtC,MAAM;KACN,MAAM;KACN,SAAS;KACT,OAAO;KACP,MAAM;IACR;IAOA,YAAY;KACV,UAAU;KACV,SAAS;;;EAPQ,kBAAkB,KAAK,MAAM;MAC9C,MAAM,OAAO,EAAE,QAAQ,MAAM,EAAE;MAC/B,OAAO,eAAe,EAAE,MAAM,QAAQ,MAAM;KAC9C,CAAC,CAAC,CAAC,KAAK,IAOL,EAAE;;;IAGL,CAAC;GACH;EACF;CACF;AACF;;;AC5BA,MAAM,uBAAuB;AAI7B,MAAM,sBAAsB;;;;;AAM5B,SAAS,oBAAoB,UAAwC;CACnE,IAAI,CAAC,SAAS,WAAW,oBAAoB,GAC3C;CAGF,IAAI,CAAC,KAAK,aAAa,GAAG;EACxB,QAAQ,KAAK,uDAAuD,UAAU;EAE9E;CACF;CAEA,MAAM,OAAO,SAAS,MAAM,CAA2B;CAKvD,MAAM,WAAW,KAAK,QAAQ,GAAG;CAEjC,IAAI,aAAa,IACf;CAGF,MAAM,SAAS,KAAK,MAAM,GAAG,QAAQ;CAKrC,IAAI,CAFW,KAAK,UAEV,CAAC,CAAC,QAAQ,SAAS,MAAM,GACjC,OAAO,IAAI,SAAS,wBAAwB;EAC1C,QAAQ;EACR,SAAS,EAAE,gBAAgB,yBAAyB;CACtD,CAAC;CAGH,MAAM,OAAO,KAAK,MAAM,WAAW,CAAC;CAEpC,IAAI,CAAC,KAAK,SAAS,KAAK,GACtB;CAGF,MAAM,YAAY,KAAK,MAAM,GAAG,EAAE;CAClC,MAAM,aAAa,UAAU,YAAY,GAAG;CAE5C,IAAI,eAAe,IACjB;CAGF,MAAM,YAAY,UAAU,MAAM,GAAG,UAAU;CAC/C,MAAM,OAAO,KAAK,aAAa,QAAQ,SAAS;CAEhD,IAAI,CAAC,MACH,OAAO,IAAI,SAAS,uBAAuB,UAAU,MAAM;EACzD,QAAQ;EACR,SAAS,EAAE,gBAAgB,yBAAyB;CACtD,CAAC;CAGH,OAAO,IAAI,SAAS,MAAkB;EACpC,QAAQ;EACR,SAAS;GACP,gBAAgB;GAChB,kBAAkB,OAAO,KAAK,UAAU;GACxC,iBAAiB;EACnB;CACF,CAAC;AACH;;;;;;;;;;;;;;;;;;;;AAqBA,SAAgB,4BAA+C;CAC7D,QAAQ,KAAK,SAAS;EACpB,MAAM,WAAW,oBAAoB,IAAI,IAAI,QAAQ;EAErD,IAAI,CAAC,UACH,OAAO,KAAK;EAId,qBAAqB,mBAAmB;EAExC,OAAO;CACT;AACF;;;;;;;;;;;;;;;;;;;;AAqBA,SAAgB,qBAAqB,SAAmD;CACtF,QAAQ,KAAK,SAAS;EACpB,IAAI,cAAc,KAAK,QAAQ,WAAW,oBAAoB,GAC5D,OAAO,KAAK;EAGd,IAAI,CAAC,KAAK,aAAa,GAAG;GACxB,QAAQ,KAAK,uDAAuD,IAAI,IAAI,UAAU;GAEtF,OAAO,KAAK;EACd;EAEA,MAAM,SAAS,QAAQ,OAAO,GAAG;EASjC,OAAO,eAAe;GANpB;GACA,cAAc,KAAK,wBAAwB,MAAM;GACjD,iBAAiB,KAAK,gBAAgB,MAAM;GAC5C,UAAU,KAAK,UAAU,CAAC,CAAC;EAGD,SAAS,KAAK,CAAC;CAC7C;AACF;;;;;;;ACpLA,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,GAAG,CAAC,CAAC,EAAE,EAAE,YAAY;GAIhD,GAAG,WAAW,EAAE,QAAQ,MAAM,EAAE,CAAC;EACnC;CACF,CAAC,CAAC,CACD,QAAQ,MAAwC,CAAC,CAAC,EAAE,IAAI,CAAC,CACzD,MAAM,GAAG,MAAM,EAAE,IAAI,EAAE,CAAC;CAE3B,KAAK,MAAM,EAAE,UAAU,WACrB,IAAI,QAAQ,SAAS,IAAI,GACvB,OAAO;AAKb"}
|
|
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/`\n * translation chunks. Client state delivery and translation preloading happen at\n * runtime: the i18n middleware bootstraps `window.__i18n__`, and the islands\n * emitter (via `@astroscope/node`) slices hashes and preloads translation chunks\n * per island — plain `client:*` directives, no forks.\n */\nexport default function i18nIntegration(options: I18nOptions = {}): AstroIntegration {\n const { consistency = 'error' } = 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\n updateConfig({\n vite: {\n plugins: [i18nVitePlugin({ logger, consistency })],\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('-')[0]?.toLowerCase();\n\n return {\n lang: baseLang,\n q: parseFloat(q.replace('q=', '')),\n };\n })\n .filter((p): p is { lang: string; q: number } => !!p.lang)\n .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":";;;;;;;;;;;;;;AAcA,SAAwB,gBAAgB,UAAuB,CAAC,GAAqB;CACnF,MAAM,EAAE,cAAc,YAAY;CAElC,OAAO;EACL,MAAM;EACN,OAAO,EACL,uBAAuB,EAAE,eAAe,cAAc,aAAa;GACjE,cAAc;IAAE,OAAO;IAAO,YAAY;GAAoC,CAAC;GAE/E,aAAa,EACX,MAAM,EACJ,SAAS,CAAC,eAAe;IAAE;IAAQ;GAAY,CAAC,CAAC,EACnD,EACF,CAAC;EACH,EACF;CACF;AACF;;;;;;;ACzBA,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,GAAG,CAAC,CAAC,EAAE,EAAE,YAAY;GAIhD,GAAG,WAAW,EAAE,QAAQ,MAAM,EAAE,CAAC;EACnC;CACF,CAAC,CAAC,CACD,QAAQ,MAAwC,CAAC,CAAC,EAAE,IAAI,CAAC,CACzD,MAAM,GAAG,MAAM,EAAE,IAAI,EAAE,CAAC;CAE3B,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 { i as I18nContext, n as i18n } from "./i18n-
|
|
1
|
+
import { n as I18nClientState } from "./types-DU8QHch4.js";
|
|
2
|
+
import { i as I18nContext, n as i18n } from "./i18n-Bp8TFmd3.js";
|
|
3
3
|
//#region src/server/context.d.ts
|
|
4
4
|
/**
|
|
5
5
|
* Get the current i18n context
|
package/dist/internal.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { r as i18n, t as getContext } from "./context-
|
|
1
|
+
import { r as i18n, t as getContext } from "./context-CYqyZJyH.js";
|
|
2
2
|
export { getContext, i18n };
|
|
@@ -11,7 +11,7 @@ function getGlobalState() {
|
|
|
11
11
|
if (!g[I18N_MANIFEST_GLOBAL_KEY]) g[I18N_MANIFEST_GLOBAL_KEY] = {
|
|
12
12
|
extractedKeys: [],
|
|
13
13
|
chunkManifest: {},
|
|
14
|
-
|
|
14
|
+
scriptChunks: [],
|
|
15
15
|
projectRoot: "",
|
|
16
16
|
version: 0
|
|
17
17
|
};
|
|
@@ -35,10 +35,10 @@ function getManifest() {
|
|
|
35
35
|
})
|
|
36
36
|
})),
|
|
37
37
|
chunks: state.chunkManifest,
|
|
38
|
-
|
|
38
|
+
scripts: state.scriptChunks
|
|
39
39
|
};
|
|
40
40
|
}
|
|
41
41
|
//#endregion
|
|
42
42
|
export { getManifest as n, getGlobalState as t };
|
|
43
43
|
|
|
44
|
-
//# sourceMappingURL=manifest-
|
|
44
|
+
//# sourceMappingURL=manifest-Cma68XM-.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"manifest-Cma68XM-.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 // in dev mode, initialize global state if not present\n if (!g[I18N_MANIFEST_GLOBAL_KEY]) {\n g[I18N_MANIFEST_GLOBAL_KEY] = {\n extractedKeys: [],\n chunkManifest: {},\n scriptChunks: [],\n projectRoot: '',\n version: 0,\n };\n }\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;CAGV,IAAI,CAAC,EAAE,2BACL,EAAE,4BAA4B;EAC5B,eAAe,CAAC;EAChB,eAAe,CAAC;EAChB,cAAc,CAAC;EACf,aAAa;EACb,SAAS;CACX;CAGF,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"}
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import { t as buildI18nChunkUrl } from "./url-CifeaDzl.js";
|
|
2
|
+
import { n as runWithContext, r as i18n } from "./context-CYqyZJyH.js";
|
|
3
|
+
import { RECOMMENDED_EXCLUDES, shouldExclude } from "@astroscope/node/excludes";
|
|
4
|
+
import { log, overrideRequestRoute } from "@astroscope/node/log";
|
|
5
|
+
import { registerDocumentEmitter, registerIslandEmitter } from "@astroscope/node/islands";
|
|
6
|
+
//#region src/server/client-state.ts
|
|
7
|
+
/**
|
|
8
|
+
* Scripts bootstrapping `window.__i18n__` on html responses, delivered through the
|
|
9
|
+
* `@astroscope/node` document emitter (see islands-emitter.ts for the placement
|
|
10
|
+
* policy). Both writers merge orderlessly (`??=` + assign), so emitter scripts and
|
|
11
|
+
* the bootstrap can arrive in either order.
|
|
12
|
+
*/
|
|
13
|
+
/** translation values may contain `<\/script>` — escape `<` so they cannot end the tag */
|
|
14
|
+
function jsonForScript(value) {
|
|
15
|
+
return JSON.stringify(value).replace(/</g, "\\u003c");
|
|
16
|
+
}
|
|
17
|
+
/** the full dev-mode state: every translation for the locale */
|
|
18
|
+
function createFullStateScript(locale) {
|
|
19
|
+
return `<script>window.__i18n__=Object.assign(window.__i18n__??{hashes:{},translations:{}},${jsonForScript({
|
|
20
|
+
locale,
|
|
21
|
+
hashes: {},
|
|
22
|
+
translations: i18n.getTranslations(locale)
|
|
23
|
+
})});<\/script>`;
|
|
24
|
+
}
|
|
25
|
+
/** the prod bootstrap: chunk hashes for astro `<script>` entry consumers */
|
|
26
|
+
function createScriptHashesScript(locale) {
|
|
27
|
+
const hashes = i18n.getScriptChunkHashes(locale);
|
|
28
|
+
if (Object.keys(hashes).length === 0) return null;
|
|
29
|
+
return `<script>{const i=window.__i18n__??=${jsonForScript({
|
|
30
|
+
locale,
|
|
31
|
+
hashes: {},
|
|
32
|
+
translations: {}
|
|
33
|
+
})};Object.assign(i.hashes,${jsonForScript(hashes)});}<\/script>`;
|
|
34
|
+
}
|
|
35
|
+
//#endregion
|
|
36
|
+
//#region src/server/islands-emitter.ts
|
|
37
|
+
/**
|
|
38
|
+
* Contributes the i18n slice for each island the `@astroscope/node` islands
|
|
39
|
+
* middleware streams past:
|
|
40
|
+
*
|
|
41
|
+
* - an inline merge script with the hashes of the island's full chunk closure —
|
|
42
|
+
* dynamic imports included, since a `React.lazy` chunk's injected loader reads
|
|
43
|
+
* its hash at execution time. Emitted before the island tag, it is parsed
|
|
44
|
+
* strictly before the island connects, and the island's chunks execute at
|
|
45
|
+
* least a network round-trip later — so each island's hashes win their race
|
|
46
|
+
* even while the rest of the document is still streaming. Chunks already
|
|
47
|
+
* covered earlier in the document are skipped.
|
|
48
|
+
* - translation-chunk preload links for the static closure, emitted as link tags
|
|
49
|
+
* for immediate islands.
|
|
50
|
+
* - translation-chunk eager imports for the full closure — dynamic imports
|
|
51
|
+
* included — fired by the deferred gate the moment a directive schedules
|
|
52
|
+
* hydration. Translation chunks are idempotent data modules, so evaluating
|
|
53
|
+
* them ahead of the component costs nothing, and the loader's awaited import
|
|
54
|
+
* then hits the module cache instead of fetching serially after the component
|
|
55
|
+
* graph evaluated. Every island carries its complete i18n set (no per-document
|
|
56
|
+
* dedup — the gate dedups at fire time), so a chunk shared between islands is
|
|
57
|
+
* covered by whichever gate fires first.
|
|
58
|
+
*
|
|
59
|
+
* The locale is recorded per request by the i18n middleware — the emitter runs
|
|
60
|
+
* while the response streams, outside the middleware's AsyncLocalStorage scope.
|
|
61
|
+
*/
|
|
62
|
+
const requestLocales = /* @__PURE__ */ new WeakMap();
|
|
63
|
+
const emittedByContext = /* @__PURE__ */ new WeakMap();
|
|
64
|
+
const IS_DEV = !!import.meta.env?.DEV;
|
|
65
|
+
let registered = false;
|
|
66
|
+
function setRequestLocale(request, locale) {
|
|
67
|
+
requestLocales.set(request, locale);
|
|
68
|
+
}
|
|
69
|
+
/** public url of a chunk → manifest chunk name, e.g. `…/_astro/Cart.abc.js` → `Cart.abc` */
|
|
70
|
+
function urlToChunkName(url) {
|
|
71
|
+
return url.slice(url.lastIndexOf("/") + 1).replace(/\.js$/, "");
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* The document-level bootstrap for `window.__i18n__`, exported for tests.
|
|
75
|
+
*/
|
|
76
|
+
function createI18nDocumentEmitter(dev = IS_DEV) {
|
|
77
|
+
return (context) => {
|
|
78
|
+
if (!i18n.isConfigured()) return null;
|
|
79
|
+
const locale = requestLocales.get(context.request);
|
|
80
|
+
if (locale === void 0) return null;
|
|
81
|
+
if (dev) return { head: createFullStateScript(locale) };
|
|
82
|
+
if (Object.keys(i18n.getManifest().chunks).length === 0) return null;
|
|
83
|
+
return { end: () => createScriptHashesScript(locale) };
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
function registerI18nEmitters() {
|
|
87
|
+
if (registered) return;
|
|
88
|
+
registered = true;
|
|
89
|
+
registerDocumentEmitter(createI18nDocumentEmitter());
|
|
90
|
+
registerIslandEmitter((island, context) => {
|
|
91
|
+
if (!context || !i18n.isConfigured()) return null;
|
|
92
|
+
const locale = requestLocales.get(context.request);
|
|
93
|
+
if (locale === void 0) return null;
|
|
94
|
+
const hashes = i18n.getHashes(locale);
|
|
95
|
+
let emitted = emittedByContext.get(context);
|
|
96
|
+
if (!emitted) {
|
|
97
|
+
emitted = /* @__PURE__ */ new Set();
|
|
98
|
+
emittedByContext.set(context, emitted);
|
|
99
|
+
}
|
|
100
|
+
const merge = {};
|
|
101
|
+
for (const url of island.fullClosure) {
|
|
102
|
+
const chunk = urlToChunkName(url);
|
|
103
|
+
const hash = hashes[chunk];
|
|
104
|
+
if (hash && !emitted.has(chunk)) {
|
|
105
|
+
emitted.add(chunk);
|
|
106
|
+
merge[chunk] = hash;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
const links = [];
|
|
110
|
+
for (const url of island.staticClosure) {
|
|
111
|
+
const chunk = urlToChunkName(url);
|
|
112
|
+
const hash = hashes[chunk];
|
|
113
|
+
if (hash) links.push(buildI18nChunkUrl(locale, chunk, hash));
|
|
114
|
+
}
|
|
115
|
+
const imports = [];
|
|
116
|
+
for (const url of island.fullClosure) {
|
|
117
|
+
const chunk = urlToChunkName(url);
|
|
118
|
+
const hash = hashes[chunk];
|
|
119
|
+
if (hash) imports.push(buildI18nChunkUrl(locale, chunk, hash));
|
|
120
|
+
}
|
|
121
|
+
const init = jsonForScript({
|
|
122
|
+
locale,
|
|
123
|
+
hashes: {},
|
|
124
|
+
translations: {}
|
|
125
|
+
});
|
|
126
|
+
const html = Object.keys(merge).length > 0 ? `<script>{const i=window.__i18n__??=${init};Object.assign(i.hashes,${jsonForScript(merge)});}<\/script>` : void 0;
|
|
127
|
+
if (!html && links.length === 0 && imports.length === 0) return null;
|
|
128
|
+
return {
|
|
129
|
+
html,
|
|
130
|
+
links: links.length > 0 ? links : void 0,
|
|
131
|
+
imports: imports.length > 0 ? imports : void 0
|
|
132
|
+
};
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
//#endregion
|
|
136
|
+
//#region src/server/middleware.ts
|
|
137
|
+
const I18N_ENDPOINT_PREFIX = "/_i18n/";
|
|
138
|
+
const I18N_ENDPOINT_ROUTE = "/_i18n/[locale]/[chunk]";
|
|
139
|
+
/**
|
|
140
|
+
* Serve a translation chunk. Returns `undefined` when the path isn't a chunk
|
|
141
|
+
* request, and the caller passes it on to astro.
|
|
142
|
+
*/
|
|
143
|
+
function createChunkResponse(pathname) {
|
|
144
|
+
if (!pathname.startsWith(I18N_ENDPOINT_PREFIX)) return;
|
|
145
|
+
if (!i18n.isConfigured()) {
|
|
146
|
+
log.error("i18n not configured, passing through");
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
const path = pathname.slice(7);
|
|
150
|
+
const slashIdx = path.indexOf("/");
|
|
151
|
+
if (slashIdx === -1) return;
|
|
152
|
+
const locale = path.slice(0, slashIdx);
|
|
153
|
+
if (!i18n.getConfig().locales.includes(locale)) return new Response("/* unknown locale */", {
|
|
154
|
+
status: 404,
|
|
155
|
+
headers: { "Content-Type": "application/javascript" }
|
|
156
|
+
});
|
|
157
|
+
const rest = path.slice(slashIdx + 1);
|
|
158
|
+
if (!rest.endsWith(".js")) return;
|
|
159
|
+
const withoutJs = rest.slice(0, -3);
|
|
160
|
+
const lastDotIdx = withoutJs.lastIndexOf(".");
|
|
161
|
+
if (lastDotIdx === -1) return;
|
|
162
|
+
const chunkName = withoutJs.slice(0, lastDotIdx);
|
|
163
|
+
const body = i18n.getChunkBody(locale, chunkName);
|
|
164
|
+
if (!body) return new Response(`/* chunk not found: ${chunkName} */`, {
|
|
165
|
+
status: 404,
|
|
166
|
+
headers: { "Content-Type": "application/javascript" }
|
|
167
|
+
});
|
|
168
|
+
return new Response(body, {
|
|
169
|
+
status: 200,
|
|
170
|
+
headers: {
|
|
171
|
+
"Content-Type": "text/javascript; charset=utf-8",
|
|
172
|
+
"Content-Length": String(body.byteLength),
|
|
173
|
+
"Cache-Control": "public, max-age=31536000, immutable"
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Create the i18n chunk middleware that serves translation chunks at `/_i18n/` endpoints.
|
|
179
|
+
*
|
|
180
|
+
* Injected by the integration as `pre` middleware (before any user middleware) —
|
|
181
|
+
* not part of the public API.
|
|
182
|
+
*
|
|
183
|
+
* @internal
|
|
184
|
+
*/
|
|
185
|
+
function createI18nChunkMiddleware() {
|
|
186
|
+
return (ctx, next) => {
|
|
187
|
+
const response = createChunkResponse(ctx.url.pathname);
|
|
188
|
+
if (!response) return next();
|
|
189
|
+
overrideRequestRoute(I18N_ENDPOINT_ROUTE);
|
|
190
|
+
return response;
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Create the i18n locale middleware.
|
|
195
|
+
*
|
|
196
|
+
* Sets up the request context with locale and translations for `t()`, injects
|
|
197
|
+
* the client i18n state into html responses, and records the locale for the
|
|
198
|
+
* islands emitter. Place this after session middleware if your locale detection
|
|
199
|
+
* depends on session/cookies.
|
|
200
|
+
*
|
|
201
|
+
* @example
|
|
202
|
+
* ```typescript
|
|
203
|
+
* import { createI18nMiddleware, detectLocale, i18n } from '@astroscope/i18n';
|
|
204
|
+
*
|
|
205
|
+
* export const i18nMiddleware = createI18nMiddleware({
|
|
206
|
+
* locale: (ctx) =>
|
|
207
|
+
* ctx.cookies.get('locale')?.value ??
|
|
208
|
+
* detectLocale(ctx.request) ??
|
|
209
|
+
* i18n.getConfig().defaultLocale,
|
|
210
|
+
* });
|
|
211
|
+
* ```
|
|
212
|
+
*/
|
|
213
|
+
function createI18nMiddleware(options) {
|
|
214
|
+
registerI18nEmitters();
|
|
215
|
+
return async (ctx, next) => {
|
|
216
|
+
if (shouldExclude(ctx, options.exclude ?? RECOMMENDED_EXCLUDES)) return next();
|
|
217
|
+
if (!i18n.isConfigured()) {
|
|
218
|
+
log.error("i18n not configured, passing through");
|
|
219
|
+
return next();
|
|
220
|
+
}
|
|
221
|
+
const locale = options.locale(ctx);
|
|
222
|
+
setRequestLocale(ctx.request, locale);
|
|
223
|
+
const context = {
|
|
224
|
+
locale,
|
|
225
|
+
translations: i18n.getCompiledTranslations(locale),
|
|
226
|
+
rawTranslations: i18n.getTranslations(locale),
|
|
227
|
+
fallback: i18n.getConfig().fallback
|
|
228
|
+
};
|
|
229
|
+
return runWithContext(context, () => next());
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
//#endregion
|
|
233
|
+
export { createI18nMiddleware as n, createI18nChunkMiddleware as t };
|
|
234
|
+
|
|
235
|
+
//# sourceMappingURL=middleware-DxKY84Iv.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"middleware-DxKY84Iv.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).replace(/</g, '\\\\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 { type DocumentEmitter, registerDocumentEmitter, registerIslandEmitter } 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, emitted as link tags\n * for immediate islands.\n * - translation-chunk eager imports for the full closure — dynamic imports\n * included — fired by the deferred gate the moment a directive schedules\n * hydration. Translation chunks are idempotent data modules, so evaluating\n * them ahead of the component costs nothing, and the loader's awaited import\n * then hits the module cache instead of fetching serially after the component\n * graph evaluated. Every island carries its complete i18n set (no per-document\n * dedup — the gate dedups at fire time), so a chunk shared between islands is\n * covered by whichever 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 */\n\nconst requestLocales = new WeakMap<Request, string>();\nconst emittedByContext = new WeakMap<APIContext, 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(request: Request, locale: string): void {\n requestLocales.set(request, 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/**\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.request);\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.request);\n\n if (locale === undefined) {\n return null;\n }\n\n const hashes = i18n.getHashes(locale);\n\n let emitted = emittedByContext.get(context);\n\n if (!emitted) {\n emitted = new Set();\n emittedByContext.set(context, emitted);\n }\n\n const merge: Record<string, string> = {};\n\n for (const url of island.fullClosure) {\n const chunk = urlToChunkName(url);\n const hash = hashes[chunk];\n\n if (hash && !emitted.has(chunk)) {\n emitted.add(chunk);\n merge[chunk] = hash;\n }\n }\n\n const links: string[] = [];\n\n for (const url of island.staticClosure) {\n const chunk = urlToChunkName(url);\n const hash = hashes[chunk];\n\n if (hash) {\n links.push(buildI18nChunkUrl(locale, chunk, hash));\n }\n }\n\n const imports: string[] = [];\n\n for (const url of island.fullClosure) {\n const chunk = urlToChunkName(url);\n const hash = hashes[chunk];\n\n if (hash) {\n imports.push(buildI18nChunkUrl(locale, chunk, hash));\n }\n }\n\n const init = jsonForScript({ locale, hashes: {}, translations: {} });\n const html =\n Object.keys(merge).length > 0\n ? `<script>{const i=window.__i18n__??=${init};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 { APIContext, 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 I18nMiddlewareOptions = {\n locale: (ctx: APIContext) => string;\n /**\n * Patterns to exclude from locale context setup.\n * Defaults to RECOMMENDED_EXCLUDES if not provided.\n *\n * @example\n * ```ts\n * import { RECOMMENDED_EXCLUDES } from '@astroscope/node/excludes';\n *\n * // extend defaults\n * createI18nMiddleware({\n * locale: (ctx) => ctx.locals.session?.locale ?? 'en',\n * exclude: [...RECOMMENDED_EXCLUDES, { exact: '/health' }],\n * })\n *\n * // disable excludes entirely\n * createI18nMiddleware({\n * locale: (ctx) => ctx.locals.session?.locale ?? 'en',\n * exclude: [],\n * })\n * ```\n */\n exclude?: ExcludePattern[] | ((context: APIContext) => boolean) | 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 // attempt to parse as efficient as possible\n // expected path format: {locale}/{chunkName}.{hash}.js\n // avoiding regex or split for performance\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 * Sets up the request context with locale and translations for `t()`, injects\n * the client i18n state into html responses, and records the locale for the\n * islands emitter. Place this after session middleware if your locale detection\n * depends on session/cookies.\n *\n * @example\n * ```typescript\n * import { createI18nMiddleware, detectLocale, i18n } from '@astroscope/i18n';\n *\n * export const i18nMiddleware = createI18nMiddleware({\n * locale: (ctx) =>\n * ctx.cookies.get('locale')?.value ??\n * detectLocale(ctx.request) ??\n * i18n.getConfig().defaultLocale,\n * });\n * ```\n */\nexport function createI18nMiddleware(options: I18nMiddlewareOptions): MiddlewareHandler {\n registerI18nEmitters();\n\n return async (ctx, next) => {\n if (shouldExclude(ctx, options.exclude ?? RECOMMENDED_EXCLUDES)) {\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 locale = options.locale(ctx);\n\n // the islands emitter runs while the response streams, outside this ALS scope\n setRequestLocale(ctx.request, locale);\n\n const context: I18nContext = {\n locale,\n translations: i18n.getCompiledTranslations(locale),\n rawTranslations: i18n.getTranslations(locale),\n fallback: i18n.getConfig().fallback,\n };\n\n return runWithContext(context, () => next());\n };\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rich-CHisXE-A.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\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\n for (const part of parts) {\n const target = stack.length > 0 ? stack[stack.length - 1]!.children : result;\n\n if (isMarkupPart(part)) {\n if (part.kind === 'open') {\n stack.push({ name: part.name, children: [] });\n } else if (part.kind === 'close') {\n const frame = stack.pop();\n\n if (!frame) {\n // mismatched close tag, ignore\n continue;\n }\n\n const component = components[frame.name];\n const parent = stack.length > 0 ? stack[stack.length - 1]!.children : result;\n\n if (component) {\n parent.push(addKeyIfNeeded(component(frame.children), `rich-${keyIndex++}`));\n } else {\n // no component for this tag, just flatten children\n parent.push(...frame.children);\n }\n } else if (part.kind === 'standalone') {\n // self-closing: {#icon/}\n const component = components[part.name];\n\n if (component) {\n target.push(addKeyIfNeeded(component([]), `rich-${keyIndex++}`));\n }\n }\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 isolates - skip them (they're control characters for text direction)\n // could optionally include them for proper BiDi handling\n } else {\n // handle other part types (number, string, fallback, unknown, etc.)\n // they all have a value property we can stringify\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[stack.length - 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;;;;;;;;;;;;;;;;AAiBA,SAAgB,aAAgB,OAAe,YAA+C;CAC5F,MAAM,SAAyB,CAAC;CAChC,MAAM,QAAyB,CAAC;CAChC,IAAI,WAAW;CAEf,KAAK,MAAM,QAAQ,OAAO;EACxB,MAAM,SAAS,MAAM,SAAS,IAAI,MAAM,MAAM,SAAS,EAAE,CAAE,WAAW;EAEtE,IAAI,aAAa,IAAI;
|
|
1
|
+
{"version":3,"file":"rich-CHisXE-A.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\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\n for (const part of parts) {\n const target = stack.length > 0 ? stack[stack.length - 1]!.children : result;\n\n if (isMarkupPart(part)) {\n if (part.kind === 'open') {\n stack.push({ name: part.name, children: [] });\n } else if (part.kind === 'close') {\n const frame = stack.pop();\n\n if (!frame) {\n // mismatched close tag, ignore\n continue;\n }\n\n const component = components[frame.name];\n const parent = stack.length > 0 ? stack[stack.length - 1]!.children : result;\n\n if (component) {\n parent.push(addKeyIfNeeded(component(frame.children), `rich-${keyIndex++}`));\n } else {\n // no component for this tag, just flatten children\n parent.push(...frame.children);\n }\n } else if (part.kind === 'standalone') {\n // self-closing: {#icon/}\n const component = components[part.name];\n\n if (component) {\n target.push(addKeyIfNeeded(component([]), `rich-${keyIndex++}`));\n }\n }\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 isolates - skip them (they're control characters for text direction)\n // could optionally include them for proper BiDi handling\n } else {\n // handle other part types (number, string, fallback, unknown, etc.)\n // they all have a value property we can stringify\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[stack.length - 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;;;;;;;;;;;;;;;;AAiBA,SAAgB,aAAgB,OAAe,YAA+C;CAC5F,MAAM,SAAyB,CAAC;CAChC,MAAM,QAAyB,CAAC;CAChC,IAAI,WAAW;CAEf,KAAK,MAAM,QAAQ,OAAO;EACxB,MAAM,SAAS,MAAM,SAAS,IAAI,MAAM,MAAM,SAAS,EAAE,CAAE,WAAW;EAEtE,IAAI,aAAa,IAAI,GAAG;GACtB,IAAI,KAAK,SAAS,QAChB,MAAM,KAAK;IAAE,MAAM,KAAK;IAAM,UAAU,CAAC;GAAE,CAAC;QACvC,IAAI,KAAK,SAAS,SAAS;IAChC,MAAM,QAAQ,MAAM,IAAI;IAExB,IAAI,CAAC,OAEH;IAGF,MAAM,YAAY,WAAW,MAAM;IACnC,MAAM,SAAS,MAAM,SAAS,IAAI,MAAM,MAAM,SAAS,EAAE,CAAE,WAAW;IAEtE,IAAI,WACF,OAAO,KAAK,eAAe,UAAU,MAAM,QAAQ,GAAG,QAAQ,YAAY,CAAC;SAG3E,OAAO,KAAK,GAAG,MAAM,QAAQ;GAEjC,OAAO,IAAI,KAAK,SAAS,cAAc;IAErC,MAAM,YAAY,WAAW,KAAK;IAElC,IAAI,WACF,OAAO,KAAK,eAAe,UAAU,CAAC,CAAC,GAAG,QAAQ,YAAY,CAAC;GAEnE;EACF,OAAO,IAAI,KAAK,SAAS,QAEvB,OAAO,KAAK,KAAK,KAAe;OAC3B,IAAI,KAAK,SAAS,iBAAiB,CAG1C,OAGE,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,MAAM,SAAS,EAAE,CAAE,WAAW,OAAA,CAE/D,KAAK,GAAG,MAAM,QAAQ;CAC/B;CAEA,OAAO;AACT"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"chunk-middleware-entrypoint.d.ts","names":[],"sources":["../../src/server/chunk-middleware-entrypoint.ts"],"mappings":";cAEa,2BAAS"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { t as createI18nChunkMiddleware } from "../middleware-DxKY84Iv.js";
|
|
2
|
+
//#region src/server/chunk-middleware-entrypoint.ts
|
|
3
|
+
const onRequest = createI18nChunkMiddleware();
|
|
4
|
+
//#endregion
|
|
5
|
+
export { onRequest };
|
|
6
|
+
|
|
7
|
+
//# sourceMappingURL=chunk-middleware-entrypoint.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"chunk-middleware-entrypoint.js","names":[],"sources":["../../src/server/chunk-middleware-entrypoint.ts"],"sourcesContent":["import { createI18nChunkMiddleware } from './middleware.js';\n\nexport const onRequest = createI18nChunkMiddleware();\n"],"mappings":";;AAEA,MAAa,YAAY,0BAA0B"}
|
|
@@ -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-DU8QHch4.js";
|
|
3
3
|
//#region src/server/translate.d.ts
|
|
4
4
|
/**
|
|
5
5
|
* Get the current locale from request context.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"translate.d.ts","names":[],"sources":["../../src/server/translate.ts"],"mappings":";;;;;;;iBAmCgB;;;;;;;;cAYH,GAAG;;;;;;;;;;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"translate.d.ts","names":[],"sources":["../../src/server/translate.ts"],"mappings":";;;;;;;iBAmCgB;;;;;;;;cAYH,GAAG;;;;;;;;;;;;;;;;;;;;iBA4DA,KAAK,aACnB,aACA,OAAO,sCACP,aAAa,eAAe,gBAC5B,SAAS,gDACE"}
|
package/dist/server/translate.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { r as i18n, t as getContext } from "../context-
|
|
1
|
+
import { r as i18n, t as getContext } from "../context-CYqyZJyH.js";
|
|
2
2
|
import { r as formatMessageToParts, t as compileMessage } from "../compiler-CkOVGWU5.js";
|
|
3
3
|
import { n as normalizeMeta, t as partsToNodes } from "../rich-CHisXE-A.js";
|
|
4
4
|
//#region src/server/translate.ts
|
|
@@ -74,7 +74,10 @@ const t = ((key, meta, values) => {
|
|
|
74
74
|
function rich(key, meta, components, values) {
|
|
75
75
|
const normalizedMeta = meta ? normalizeMeta(meta) : { fallback: "" };
|
|
76
76
|
const ctx = getContext();
|
|
77
|
-
|
|
77
|
+
const locale = ctx?.locale ?? i18n.getConfig().defaultLocale;
|
|
78
|
+
const raw = ctx?.rawTranslations[key] || normalizedMeta.fallback || key;
|
|
79
|
+
const parts = formatMessageToParts(locale, raw, values);
|
|
80
|
+
return partsToNodes(parts, components ?? {});
|
|
78
81
|
}
|
|
79
82
|
//#endregion
|
|
80
83
|
export { getLocale, rich, t };
|