@astroscope/i18n 0.6.1 → 1.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 +5 -4
- package/dist/client/translate.js +1 -1
- package/dist/client/translate.js.map +1 -1
- package/dist/{context-DT91WgEO.js → context-DLfoS-uR.js} +3 -3
- package/dist/context-DLfoS-uR.js.map +1 -0
- package/dist/extraction/index.js +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +6 -7
- package/dist/index.js.map +1 -1
- package/dist/internal.js +1 -1
- package/dist/{manifest-D883_BIS.js → manifest-BEm6GezL.js} +3 -2
- package/dist/{manifest-D883_BIS.js.map → manifest-BEm6GezL.js.map} +1 -1
- package/dist/server/translate.js +2 -2
- package/dist/server/translate.js.map +1 -1
- package/dist/{vite-plugin-DVUlcvEH.js → vite-plugin-CXUN45oF.js} +2 -2
- package/dist/{vite-plugin-DVUlcvEH.js.map → vite-plugin-CXUN45oF.js.map} +1 -1
- package/package.json +3 -3
- package/dist/context-DT91WgEO.js.map +0 -1
package/README.md
CHANGED
|
@@ -31,7 +31,7 @@ i18n for Astro + React islands — automatic tree-shaking, parallel loading, any
|
|
|
31
31
|
## Installation
|
|
32
32
|
|
|
33
33
|
```bash
|
|
34
|
-
npm install @astroscope/i18n @astroscope/
|
|
34
|
+
npm install @astroscope/i18n @astroscope/node
|
|
35
35
|
```
|
|
36
36
|
|
|
37
37
|
## Usage
|
|
@@ -41,12 +41,13 @@ npm install @astroscope/i18n @astroscope/boot @astroscope/excludes
|
|
|
41
41
|
```ts
|
|
42
42
|
// astro.config.ts
|
|
43
43
|
import { defineConfig } from 'astro/config';
|
|
44
|
-
import
|
|
44
|
+
import node from '@astroscope/node';
|
|
45
45
|
import i18n from '@astroscope/i18n';
|
|
46
46
|
|
|
47
47
|
export default defineConfig({
|
|
48
|
+
output: 'server',
|
|
49
|
+
adapter: node(),
|
|
48
50
|
integrations: [
|
|
49
|
-
boot(),
|
|
50
51
|
i18n(),
|
|
51
52
|
],
|
|
52
53
|
});
|
|
@@ -119,7 +120,7 @@ By default, `RECOMMENDED_EXCLUDES` (static assets like `/_astro/`) are excluded
|
|
|
119
120
|
```ts
|
|
120
121
|
import { sequence } from 'astro:middleware';
|
|
121
122
|
import { createI18nChunkMiddleware, createI18nMiddleware, i18n } from '@astroscope/i18n';
|
|
122
|
-
import { RECOMMENDED_EXCLUDES } from '@astroscope/excludes';
|
|
123
|
+
import { RECOMMENDED_EXCLUDES } from '@astroscope/node/excludes';
|
|
123
124
|
|
|
124
125
|
export const onRequest = sequence(
|
|
125
126
|
createI18nChunkMiddleware(),
|
package/dist/client/translate.js
CHANGED
|
@@ -64,7 +64,7 @@ const t = ((key, meta, values) => {
|
|
|
64
64
|
function rich(key, meta, components, values) {
|
|
65
65
|
const normalizedMeta = meta ? normalizeMeta(meta) : { fallback: "" };
|
|
66
66
|
const state = getI18nState();
|
|
67
|
-
const raw = state?.translations[key]
|
|
67
|
+
const raw = state?.translations[key] || normalizedMeta.fallback || key;
|
|
68
68
|
return partsToNodes(formatMessageToParts(state?.locale ?? "", raw, values), components ?? {});
|
|
69
69
|
}
|
|
70
70
|
//#endregion
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"translate.js","names":[],"sources":["../../src/client/translate.ts"],"sourcesContent":["import { 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 { getI18nState } from './state.js';\n\nconst cache = new Map<string, CompiledTranslation>();\n\nexport function getLocale(): string {\n return getI18nState()?.locale ?? '';\n}\n\n/**\n * Client-side translate function\n *\n * Reads from window.__i18n__.translations (injected per-chunk).\n * In production, translations are always loaded before component code runs\n * via the chunk loader injected by the Vite plugin.\n *\n * Call patterns:\n * - Development: t('key', 'fallback') or t('key', 'fallback', values)\n * - Production: t('key') or t('key', undefined, values)\n *\n * Falls back to meta.fallback (dev) or key (prod) when translation is missing.\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 state = getI18nState();\n const locale = state?.locale ?? '';\n\n // look up raw translation\n const raw = state?.translations[key];\n\n if (!raw) {\n // translation missing: use fallback (dev) or key (prod)\n if (normalizedMeta.fallback) {\n let compiled = cache.get(`__fallback__${key}`);\n\n if (!compiled) {\n compiled = compileMessage(locale, normalizedMeta.fallback);\n cache.set(`__fallback__${key}`, compiled);\n }\n\n return compiled(values);\n }\n\n // production: return key as-is\n return key;\n }\n\n // check cache\n let compiled = cache.get(key);\n\n if (!compiled) {\n compiled = compileMessage(locale, raw);\n cache.set(key, compiled);\n }\n\n return compiled(values);\n}) as TranslateFunction;\n\n/**\n * Client-side rich text translation function\n *\n * Parses MF2 markup syntax and applies component callbacks to wrap content.\n * Works with any JSX runtime (React, Preact, 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 (ICU MessageFormat)\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 state = getI18nState();\n\n // look up raw translation, fall back to meta.fallback or key\n const raw = state?.translations[key]
|
|
1
|
+
{"version":3,"file":"translate.js","names":[],"sources":["../../src/client/translate.ts"],"sourcesContent":["import { 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 { getI18nState } from './state.js';\n\nconst cache = new Map<string, CompiledTranslation>();\n\nexport function getLocale(): string {\n return getI18nState()?.locale ?? '';\n}\n\n/**\n * Client-side translate function\n *\n * Reads from window.__i18n__.translations (injected per-chunk).\n * In production, translations are always loaded before component code runs\n * via the chunk loader injected by the Vite plugin.\n *\n * Call patterns:\n * - Development: t('key', 'fallback') or t('key', 'fallback', values)\n * - Production: t('key') or t('key', undefined, values)\n *\n * Falls back to meta.fallback (dev) or key (prod) when translation is missing.\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 state = getI18nState();\n const locale = state?.locale ?? '';\n\n // look up raw translation\n const raw = state?.translations[key];\n\n if (!raw) {\n // translation missing: use fallback (dev) or key (prod)\n if (normalizedMeta.fallback) {\n let compiled = cache.get(`__fallback__${key}`);\n\n if (!compiled) {\n compiled = compileMessage(locale, normalizedMeta.fallback);\n cache.set(`__fallback__${key}`, compiled);\n }\n\n return compiled(values);\n }\n\n // production: return key as-is\n return key;\n }\n\n // check cache\n let compiled = cache.get(key);\n\n if (!compiled) {\n compiled = compileMessage(locale, raw);\n cache.set(key, compiled);\n }\n\n return compiled(values);\n}) as TranslateFunction;\n\n/**\n * Client-side rich text translation function\n *\n * Parses MF2 markup syntax and applies component callbacks to wrap content.\n * Works with any JSX runtime (React, Preact, 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 (ICU MessageFormat)\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 state = getI18nState();\n\n // look up raw translation, fall back to meta.fallback or key\n const raw = state?.translations[key] || normalizedMeta.fallback || key;\n\n const parts = formatMessageToParts(state?.locale ?? '', raw, values);\n\n return partsToNodes(parts, components ?? {});\n}\n"],"mappings":";;;;AAMA,MAAM,wBAAQ,IAAI,IAAiC;AAEnD,SAAgB,YAAoB;CAClC,OAAO,aAAa,CAAC,EAAE,UAAU;AACnC;;;;;;;;;;;;;;AAeA,MAAa,MACX,KACA,MACA,WACW;CACX,MAAM,iBAAkC,OAAO,cAAc,IAAI,IAAI,EAAE,UAAU,GAAG;CAEpF,MAAM,QAAQ,aAAa;CAC3B,MAAM,SAAS,OAAO,UAAU;CAGhC,MAAM,MAAM,OAAO,aAAa;CAEhC,IAAI,CAAC,KAAK;EAER,IAAI,eAAe,UAAU;GAC3B,IAAI,WAAW,MAAM,IAAI,eAAe,KAAK;GAE7C,IAAI,CAAC,UAAU;IACb,WAAW,eAAe,QAAQ,eAAe,QAAQ;IACzD,MAAM,IAAI,eAAe,OAAO,QAAQ;GAC1C;GAEA,OAAO,SAAS,MAAM;EACxB;EAGA,OAAO;CACT;CAGA,IAAI,WAAW,MAAM,IAAI,GAAG;CAE5B,IAAI,CAAC,UAAU;EACb,WAAW,eAAe,QAAQ,GAAG;EACrC,MAAM,IAAI,KAAK,QAAQ;CACzB;CAEA,OAAO,SAAS,MAAM;AACxB;;;;;;;;;;;;;;;;;;;;AAqBA,SAAgB,KACd,KACA,MACA,YACA,QACgB;CAChB,MAAM,iBAAkC,OAAO,cAAc,IAAI,IAAI,EAAE,UAAU,GAAG;CAEpF,MAAM,QAAQ,aAAa;CAG3B,MAAM,MAAM,OAAO,aAAa,QAAQ,eAAe,YAAY;CAInE,OAAO,aAFO,qBAAqB,OAAO,UAAU,IAAI,KAAK,MAErC,GAAG,cAAc,CAAC,CAAC;AAC7C"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { t as getGlobalState } from "./manifest-
|
|
1
|
+
import { t as getGlobalState } from "./manifest-BEm6GezL.js";
|
|
2
2
|
import { n as compileTranslations } from "./compiler-CkOVGWU5.js";
|
|
3
3
|
import { createHash } from "node:crypto";
|
|
4
4
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
@@ -14,7 +14,7 @@ function hashString(str) {
|
|
|
14
14
|
*/
|
|
15
15
|
function computeChunkHash(translations, keys) {
|
|
16
16
|
const relevantTranslations = {};
|
|
17
|
-
for (const key of keys.sort()) if (translations[key]) relevantTranslations[key] = translations[key];
|
|
17
|
+
for (const key of [...keys].sort()) if (translations[key] !== void 0) relevantTranslations[key] = translations[key];
|
|
18
18
|
return hashString(JSON.stringify(relevantTranslations));
|
|
19
19
|
}
|
|
20
20
|
/**
|
|
@@ -244,4 +244,4 @@ function getContext() {
|
|
|
244
244
|
//#endregion
|
|
245
245
|
export { runWithContext as n, i18n as r, getContext as t };
|
|
246
246
|
|
|
247
|
-
//# sourceMappingURL=context-
|
|
247
|
+
//# sourceMappingURL=context-DLfoS-uR.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"context-DLfoS-uR.js","names":[],"sources":["../src/extraction/hash.ts","../src/server/utils.ts","../src/server/i18n.ts","../src/server/context.ts"],"sourcesContent":["import { createHash } from 'node:crypto';\nimport type { RawTranslations } from '../shared/types.js';\nimport type { ChunkManifest } from './types.js';\n\n/**\n * Hash a string to a short hash (for cache busting)\n */\nfunction hashString(str: string): string {\n return createHash('sha256').update(str).digest('hex').substring(0, 8);\n}\n\n/**\n * Compute hash for chunk translations\n */\nexport function computeChunkHash(translations: RawTranslations, keys: string[]): string {\n const relevantTranslations: RawTranslations = {};\n\n for (const key of [...keys].sort()) {\n if (translations[key] !== undefined) {\n relevantTranslations[key] = translations[key];\n }\n }\n\n return hashString(JSON.stringify(relevantTranslations));\n}\n\n/**\n * Compute hashes for all chunks in manifest\n */\nexport function computeAllChunkHashes(translations: RawTranslations, manifest: ChunkManifest): Record<string, string> {\n const hashes: Record<string, string> = {};\n\n for (const [chunkId, keys] of Object.entries(manifest)) {\n hashes[chunkId] = computeChunkHash(translations, keys);\n }\n\n return hashes;\n}\n","const LOWERCASE_A = 97;\n\n/**\n * Convert index to lowercase letter: 0→'a', 1→'b', ..., 25→'z'\n */\nfunction indexToLetter(index: number): string {\n return String.fromCharCode(LOWERCASE_A + index);\n}\n\n/**\n * Generate short variable name using bijective base-26: 0→'a', 25→'z', 26→'aa', 27→'ab', ...\n */\nexport function generateBB26(index: number): string {\n let name = '';\n let remaining = index;\n\n do {\n name = indexToLetter(remaining % 26) + name;\n remaining = Math.floor(remaining / 26) - 1;\n } while (remaining >= 0);\n\n return name;\n}\n","import { computeAllChunkHashes } from '../extraction/hash.js';\nimport { getGlobalState } from '../extraction/manifest.js';\nimport type { ExtractionManifest } from '../extraction/types.js';\nimport { compileTranslations } from '../shared/compiler.js';\nimport type { CompiledTranslations, RawTranslations } from '../shared/types.js';\nimport type { FallbackBehavior } from './types.js';\nimport { generateBB26 } from './utils.js';\n\nexport type I18nConfig = {\n locales: string[];\n defaultLocale?: string | undefined;\n fallback?: FallbackBehavior | undefined;\n};\n\ntype NormalizedConfig = {\n locales: string[];\n defaultLocale: string;\n fallback: FallbackBehavior;\n};\n\nclass I18nSingleton {\n // normalized user config with defaults applied\n private config: NormalizedConfig | null = null;\n\n // locale -> raw translation strings (before ICU compilation)\n private rawCache = new Map<string, RawTranslations>();\n\n // locale -> raw translations merged with manifest fallbacks\n private mergedCache = new Map<string, RawTranslations>();\n\n // locale -> compiled ICU MessageFormat functions\n private compiledCache = new Map<string, CompiledTranslations>();\n\n // locale -> chunk name -> content hash (for cache busting)\n private hashCache = new Map<string, Record<string, string>>();\n\n // locale -> inline script for I18nScript component\n private scriptCache = new Map<string, string>();\n\n // \"locale:chunkName\" -> encoded chunk response body\n private chunkCache = new Map<string, Uint8Array>();\n\n // manifest getter (set by init module, provides live data in dev mode)\n private manifestGetter: (() => ExtractionManifest) | null = null;\n\n // tracks the manifest version to detect when derived caches need invalidation\n private manifestVersion = 0;\n\n async configure(config: I18nConfig): Promise<void> {\n if (!config.locales?.length) {\n throw new Error('i18n.configure(): locales array is required and must not be empty');\n }\n\n for (const locale of config.locales) {\n if (!locale) {\n throw new Error('i18n.configure(): locale cannot be empty');\n }\n\n if (locale !== locale.trim()) {\n throw new Error(`i18n.configure(): locale \"${locale}\" has leading or trailing whitespace`);\n }\n }\n\n if (new Set(config.locales).size !== config.locales.length) {\n throw new Error('i18n.configure(): locales array contains duplicates');\n }\n\n if (config.defaultLocale !== undefined) {\n if (!config.defaultLocale) {\n throw new Error('i18n.configure(): defaultLocale cannot be empty');\n }\n\n if (config.defaultLocale !== config.defaultLocale.trim()) {\n throw new Error(`i18n.configure(): defaultLocale \"${config.defaultLocale}\" has leading or trailing whitespace`);\n }\n\n if (!config.locales.includes(config.defaultLocale)) {\n throw new Error(`i18n.configure(): defaultLocale \"${config.defaultLocale}\" is not in locales array`);\n }\n }\n\n this.config = {\n locales: config.locales,\n defaultLocale: config.defaultLocale ?? config.locales[0]!,\n fallback: config.fallback ?? 'fallback',\n };\n\n // dynamically import manifest getter from virtual module\n // this removes the need for a separate `import '@astroscope/i18n/init'`\n if (!this.manifestGetter) {\n const m = await import('virtual:@astroscope/i18n/manifest');\n this.manifestGetter = m.getManifest;\n }\n }\n\n isConfigured(): boolean {\n return this.config !== null;\n }\n\n getConfig(): NormalizedConfig {\n if (!this.config) {\n throw new Error('i18n not configured. Call i18n.configure() first.');\n }\n\n return this.config;\n }\n\n setTranslations(locale: string, raw: RawTranslations): void {\n this.rawCache.set(locale, raw);\n this.mergedCache.delete(locale); // invalidate merged cache\n this.compiledCache.delete(locale); // invalidate compiled cache\n this.hashCache.delete(locale); // invalidate hash cache\n this.scriptCache.delete(locale); // invalidate script cache\n\n // invalidate chunk cache entries for this locale\n for (const key of this.chunkCache.keys()) {\n if (key.startsWith(`${locale}:`)) {\n this.chunkCache.delete(key);\n }\n }\n\n // recompute hashes if chunk manifest exists (production only)\n const { chunks } = this.getManifest();\n\n if (Object.keys(chunks).length > 0) {\n this.hashCache.set(locale, computeAllChunkHashes(raw, chunks));\n }\n }\n\n getTranslations(locale: string): RawTranslations {\n this.invalidateIfManifestChanged();\n\n const cached = this.mergedCache.get(locale);\n\n if (cached) {\n return cached;\n }\n\n const raw = this.rawCache.get(locale) ?? {};\n const manifest = this.manifestGetter?.() ?? { keys: [], chunks: {}, imports: {} };\n\n if (manifest.keys.length === 0) {\n return raw;\n }\n\n // merge fallbacks for keys that don't have translations\n const withFallbacks: RawTranslations = { ...raw };\n\n for (const { key, meta } of manifest.keys) {\n if (!(key in withFallbacks) && meta.fallback) {\n withFallbacks[key] = meta.fallback;\n }\n }\n\n this.mergedCache.set(locale, withFallbacks);\n\n return withFallbacks;\n }\n\n getCompiledTranslations(locale: string): CompiledTranslations {\n const cached = this.compiledCache.get(locale);\n\n if (cached) {\n return cached;\n }\n\n const compiled = compileTranslations(locale, this.getTranslations(locale));\n\n this.compiledCache.set(locale, compiled);\n\n return compiled;\n }\n\n /**\n * Get the extraction manifest (keys + chunks).\n * - keys: translation keys extracted from codebase with metadata\n * - chunks: mapping of chunk names to their translation keys\n */\n getManifest(): ExtractionManifest {\n if (!this.manifestGetter) {\n throw new Error('i18n manifest not initialized. Call i18n.configure() first.');\n }\n\n return this.manifestGetter();\n }\n\n getHashes(locale: string): Record<string, string> {\n return this.hashCache.get(locale) ?? {};\n }\n\n /**\n * Get the encoded chunk response body for a locale/chunk combination.\n * Cached to avoid repeated JSON.stringify and encoding on each request.\n * Returns undefined if chunk not found in manifest.\n */\n getChunkBody(locale: string, chunkName: string): Uint8Array | undefined {\n const keys = this.getManifest().chunks[chunkName];\n\n if (!keys) {\n return undefined;\n }\n\n const cacheKey = `${locale}:${chunkName}`;\n const cached = this.chunkCache.get(cacheKey);\n\n if (cached) {\n return cached;\n }\n\n const translations = this.getTranslations(locale);\n const chunkTranslations: RawTranslations = {};\n\n for (const key of keys) {\n if (translations[key]) {\n chunkTranslations[key] = translations[key];\n }\n }\n\n const js = `/* @astroscope/i18n chunk: ${chunkName} */\n(function() {\n var i = window.__i18n__;\n if (i) Object.assign(i.translations, ${JSON.stringify(chunkTranslations)});\n})();\n`;\n\n const body = new TextEncoder().encode(js);\n\n this.chunkCache.set(cacheKey, body);\n\n return body;\n }\n\n /**\n * Get the inline script for I18nScript component.\n * Cached per locale, invalidated when translations change.\n */\n getClientScript(locale: string): string {\n const cached = this.scriptCache.get(locale);\n\n if (cached) {\n return cached;\n }\n\n const script = this.createClientScript(locale);\n\n this.scriptCache.set(locale, script);\n\n return script;\n }\n\n private createClientScript(locale: string): string {\n const { chunks, imports } = this.getManifest();\n const hasChunks = Object.keys(chunks).length > 0;\n\n if (!hasChunks) {\n const raw = this.getTranslations(locale);\n\n return `window.__i18n__=${JSON.stringify({ locale, hashes: {}, imports: {}, translations: raw })};`;\n }\n\n const hashes = this.getHashes(locale);\n\n // collect all unique chunk names used in hashes and imports\n const allChunks = new Set<string>(Object.keys(hashes));\n\n for (const deps of Object.values(imports)) {\n deps.forEach((d) => allChunks.add(d));\n }\n\n // generate short variable names: a, b, c, ..., z, aa, ab, ...\n const chunkToVar = new Map<string, string>();\n let varIndex = 0;\n\n for (const chunk of allChunks) {\n chunkToVar.set(chunk, generateBB26(varIndex++));\n }\n\n // build IIFE with aliases for compact output\n const varDecls = [...chunkToVar.entries()].map(([chunk, v]) => `${v}=${JSON.stringify(chunk)}`).join(',');\n\n const hashesObj = Object.entries(hashes)\n .map(([chunk, hash]) => `[${chunkToVar.get(chunk)}]:${JSON.stringify(hash)}`)\n .join(',');\n\n // only include imports that are in our chunk set (have translations for this locale)\n const importsObj = Object.entries(imports)\n .filter(([chunk]) => chunkToVar.has(chunk))\n .map(([chunk, deps]) => {\n const validDeps = deps.filter((d) => chunkToVar.has(d));\n\n return validDeps.length > 0\n ? `[${chunkToVar.get(chunk)}]:[${validDeps.map((d) => chunkToVar.get(d)).join(',')}]`\n : null;\n })\n .filter(Boolean)\n .join(',');\n\n return (\n `(()=>{var ${varDecls};` +\n `window.__i18n__={locale:${JSON.stringify(locale)},hashes:{${hashesObj}},imports:{${importsObj}},translations:{}};` +\n `})();`\n );\n }\n\n /**\n * Clear translations for specified locale(s).\n * @param locales - Locale(s) to clear, or all if not specified\n */\n clear(locales?: string | string[] | undefined): void {\n const targets = this.resolveLocales(locales);\n\n for (const locale of targets) {\n this.rawCache.delete(locale);\n this.mergedCache.delete(locale);\n this.compiledCache.delete(locale);\n this.hashCache.delete(locale);\n this.scriptCache.delete(locale);\n\n // clear chunk cache entries for this locale\n for (const key of this.chunkCache.keys()) {\n if (key.startsWith(`${locale}:`)) {\n this.chunkCache.delete(key);\n }\n }\n }\n }\n\n /**\n * invalidate derived caches when the extraction manifest has changed (dev mode HMR)\n */\n private invalidateIfManifestChanged(): void {\n const { version } = getGlobalState();\n\n if (version === this.manifestVersion) {\n return;\n }\n\n this.manifestVersion = version;\n this.mergedCache.clear();\n this.compiledCache.clear();\n this.scriptCache.clear();\n this.chunkCache.clear();\n }\n\n private resolveLocales(locales?: string | string[] | undefined): string[] {\n if (!locales) {\n return this.config?.locales ?? [];\n }\n\n if (typeof locales === 'string') {\n return [locales];\n }\n\n return locales;\n }\n}\n\nexport const i18n = new I18nSingleton();\n","import { AsyncLocalStorage } from 'node:async_hooks';\nimport type { I18nContext } from './types.js';\n\nconst als = new AsyncLocalStorage<I18nContext>();\n\n/**\n * Run a function with i18n context (called by middleware)\n */\nexport function runWithContext<T>(context: I18nContext, fn: () => T): T {\n return als.run(context, fn);\n}\n\n/**\n * Get the current i18n context\n */\nexport function getContext(): I18nContext | null {\n return als.getStore() ?? null;\n}\n"],"mappings":"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"}
|
package/dist/extraction/index.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { t as i18nVitePlugin } from "../vite-plugin-
|
|
2
|
-
import { n as getManifest } from "../manifest-
|
|
1
|
+
import { t as i18nVitePlugin } from "../vite-plugin-CXUN45oF.js";
|
|
2
|
+
import { n as getManifest } from "../manifest-BEm6GezL.js";
|
|
3
3
|
export { getManifest, i18nVitePlugin };
|
package/dist/index.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { a as TranslationMeta, r as RawTranslations } from "./types-C98CcjtN.js"
|
|
|
2
2
|
import { n as ExtractedKey, r as ExtractionManifest } from "./types-BfyoJsoB.js";
|
|
3
3
|
import { n as I18nOptions } from "./types-Bo4jMK8g.js";
|
|
4
4
|
import { n as i18n, r as FallbackBehavior, t as I18nConfig } from "./i18n-DzmH9JmE.js";
|
|
5
|
-
import { ExcludePattern } from "@astroscope/excludes";
|
|
5
|
+
import { ExcludePattern } from "@astroscope/node/excludes";
|
|
6
6
|
import { APIContext, AstroIntegration, MiddlewareHandler } from "astro";
|
|
7
7
|
|
|
8
8
|
//#region src/integration/integration.d.ts
|
|
@@ -24,7 +24,7 @@ type I18nMiddlewareOptions = {
|
|
|
24
24
|
*
|
|
25
25
|
* @example
|
|
26
26
|
* ```ts
|
|
27
|
-
* import { RECOMMENDED_EXCLUDES } from '@astroscope/excludes';
|
|
27
|
+
* import { RECOMMENDED_EXCLUDES } from '@astroscope/node/excludes';
|
|
28
28
|
*
|
|
29
29
|
* // extend defaults
|
|
30
30
|
* createI18nMiddleware({
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { t as i18nVitePlugin } from "./vite-plugin-
|
|
2
|
-
import { n as runWithContext, r as i18n } from "./context-
|
|
3
|
-
import { RECOMMENDED_EXCLUDES, shouldExclude } from "@astroscope/excludes";
|
|
1
|
+
import { t as i18nVitePlugin } from "./vite-plugin-CXUN45oF.js";
|
|
2
|
+
import { n as runWithContext, r as i18n } from "./context-DLfoS-uR.js";
|
|
3
|
+
import { RECOMMENDED_EXCLUDES, shouldExclude } from "@astroscope/node/excludes";
|
|
4
4
|
//#region src/integration/integration.ts
|
|
5
5
|
const CLIENT_DIRECTIVES = [
|
|
6
6
|
"load-x",
|
|
@@ -155,13 +155,12 @@ function createI18nMiddleware(options) {
|
|
|
155
155
|
* Returns best match from configured locales, or undefined if none match.
|
|
156
156
|
*/
|
|
157
157
|
function detectLocale(request) {
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
const { locales } = config;
|
|
158
|
+
if (!i18n.isConfigured()) return void 0;
|
|
159
|
+
const { locales } = i18n.getConfig();
|
|
161
160
|
const acceptLanguage = request.headers.get("accept-language");
|
|
162
161
|
if (!acceptLanguage) return void 0;
|
|
163
162
|
const preferred = acceptLanguage.split(",").map((part) => {
|
|
164
|
-
const [lang, q = "q=1"] = part.
|
|
163
|
+
const [lang, q = "q=1"] = part.split(";").map((s) => s.trim());
|
|
165
164
|
return {
|
|
166
165
|
lang: lang?.split("-")[0]?.toLowerCase(),
|
|
167
166
|
q: parseFloat(q.replace("q=", ""))
|
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/excludes';\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/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/**\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 { pathname } = ctx.url;\n\n if (!pathname.startsWith(I18N_ENDPOINT_PREFIX)) {\n return next();\n }\n\n if (!i18n.isConfigured()) {\n console.warn(`[@astroscope/i18n] not configured, passing through: ${pathname}`);\n\n return next();\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 next();\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 next();\n }\n\n const withoutJs = rest.slice(0, -3);\n const lastDotIdx = withoutJs.lastIndexOf('.');\n\n if (lastDotIdx === -1) {\n return next();\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/**\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 const config = i18n.getConfig();\n\n if (!config) return undefined;\n\n const { locales } = config;\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 const [lang, q = 'q=1'] = part.trim().split(';');\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":"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"}
|
|
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 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/**\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 { pathname } = ctx.url;\n\n if (!pathname.startsWith(I18N_ENDPOINT_PREFIX)) {\n return next();\n }\n\n if (!i18n.isConfigured()) {\n console.warn(`[@astroscope/i18n] not configured, passing through: ${pathname}`);\n\n return next();\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 next();\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 next();\n }\n\n const withoutJs = rest.slice(0, -3);\n const lastDotIdx = withoutJs.lastIndexOf('.');\n\n if (lastDotIdx === -1) {\n return next();\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/**\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":"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"}
|
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-DLfoS-uR.js";
|
|
2
2
|
export { getContext, i18n };
|
|
@@ -27,7 +27,8 @@ function getManifest() {
|
|
|
27
27
|
keys: state.extractedKeys.map((key) => ({
|
|
28
28
|
...key,
|
|
29
29
|
files: key.files.map((fileLocation) => {
|
|
30
|
-
const
|
|
30
|
+
const match = /^(.+):(\d+)$/.exec(fileLocation);
|
|
31
|
+
const [filePath, line] = match ? [match[1], match[2]] : [fileLocation, void 0];
|
|
31
32
|
if (!filePath) return fileLocation;
|
|
32
33
|
const relativePath = state.projectRoot ? path.relative(state.projectRoot, filePath) : filePath;
|
|
33
34
|
return line ? `${relativePath}:${line}` : relativePath;
|
|
@@ -40,4 +41,4 @@ function getManifest() {
|
|
|
40
41
|
//#endregion
|
|
41
42
|
export { getManifest as n, getGlobalState as t };
|
|
42
43
|
|
|
43
|
-
//# sourceMappingURL=manifest-
|
|
44
|
+
//# sourceMappingURL=manifest-BEm6GezL.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"manifest-
|
|
1
|
+
{"version":3,"file":"manifest-BEm6GezL.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, ImportsManifest } 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 // actual manifest data\n extractedKeys: ExtractedKey[];\n chunkManifest: ChunkManifest;\n importsManifest: ImportsManifest;\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 importsManifest: {},\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, imports: state.importsManifest };\n}\n"],"mappings":";;;;;;;AAYA,MAAM,2BAA2B;AAejC,SAAgB,iBAAkC;CAChD,MAAM,IAAI;CAGV,IAAI,CAAC,EAAE,2BACL,EAAE,4BAA4B;EAC5B,eAAe,CAAC;EAChB,eAAe,CAAC;EAChB,iBAAiB,CAAC;EAClB,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;CAAgB;AAC7E"}
|
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-DLfoS-uR.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,7 @@ 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
|
-
return partsToNodes(formatMessageToParts(ctx?.locale ?? i18n.getConfig().defaultLocale, ctx?.rawTranslations[key]
|
|
77
|
+
return partsToNodes(formatMessageToParts(ctx?.locale ?? i18n.getConfig().defaultLocale, ctx?.rawTranslations[key] || normalizedMeta.fallback || key, values), components ?? {});
|
|
78
78
|
}
|
|
79
79
|
//#endregion
|
|
80
80
|
export { getLocale, rich, t };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"translate.js","names":[],"sources":["../../src/server/translate.ts"],"sourcesContent":["import { 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\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 case 'fallback':\n return meta.fallback || key;\n case 'throw':\n throw new Error(`Missing translation for key: ${key}`);\n default:\n return meta.fallback || key;\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 // no context = probably during build or outside request\n // use cached compiled fallback to avoid recompiling on every call\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 // translation missing, apply fallback\n const fallbackValue = applyFallback(key, normalizedMeta, ctx.fallback);\n\n // compile and cache the fallback for consistency\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 (ICU MessageFormat)\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 // get raw translation string - prefer from context, then fallback\n const raw = ctx?.rawTranslations[key]
|
|
1
|
+
{"version":3,"file":"translate.js","names":[],"sources":["../../src/server/translate.ts"],"sourcesContent":["import { 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\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 case 'fallback':\n return meta.fallback || key;\n case 'throw':\n throw new Error(`Missing translation for key: ${key}`);\n default:\n return meta.fallback || key;\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 // no context = probably during build or outside request\n // use cached compiled fallback to avoid recompiling on every call\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 // translation missing, apply fallback\n const fallbackValue = applyFallback(key, normalizedMeta, ctx.fallback);\n\n // compile and cache the fallback for consistency\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 (ICU MessageFormat)\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 // get raw translation string - prefer from context, then fallback, then key\n const raw = ctx?.rawTranslations[key] || normalizedMeta.fallback || key;\n\n const parts = formatMessageToParts(locale, raw, values);\n\n return partsToNodes(parts, components ?? {});\n}\n"],"mappings":";;;;AASA,MAAM,gCAAgB,IAAI,IAAiC;;;;AAK3D,SAAS,cAAc,KAAa,MAAuB,UAAoC;CAC7F,IAAI,OAAO,aAAa,YACtB,OAAO,SAAS,KAAK,IAAI;CAG3B,QAAQ,UAAR;EACE,KAAK,OACH,OAAO;EACT,KAAK,YACH,OAAO,KAAK,YAAY;EAC1B,KAAK,SACH,MAAM,IAAI,MAAM,gCAAgC,KAAK;EACvD,SACE,OAAO,KAAK,YAAY;CAC5B;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;EAGR,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;EAEb,MAAM,gBAAgB,cAAc,KAAK,gBAAgB,IAAI,QAAQ;EAGrE,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;CAQvB,OAAO,aAFO,qBALC,KAAK,UAAU,KAAK,UAAU,CAAC,CAAC,eAGnC,KAAK,gBAAgB,QAAQ,eAAe,YAAY,KAEpB,MAExB,GAAG,cAAc,CAAC,CAAC;AAC7C"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { n as chunkIdToName } from "./url-B8334XSD.js";
|
|
2
|
-
import { n as getManifest, t as getGlobalState } from "./manifest-
|
|
2
|
+
import { n as getManifest, t as getGlobalState } from "./manifest-BEm6GezL.js";
|
|
3
3
|
import fs from "node:fs";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import MagicString from "magic-string";
|
|
@@ -418,4 +418,4 @@ export function getManifest() { return _getManifest(); }
|
|
|
418
418
|
//#endregion
|
|
419
419
|
export { i18nVitePlugin as t };
|
|
420
420
|
|
|
421
|
-
//# sourceMappingURL=vite-plugin-
|
|
421
|
+
//# sourceMappingURL=vite-plugin-CXUN45oF.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"vite-plugin-DVUlcvEH.js","names":[],"sources":["../src/extraction/extract.ts","../src/extraction/key-store.ts","../src/extraction/scan.ts","../src/extraction/vite-plugin.ts"],"sourcesContent":["import type { FileResult, PluginItem } from '@babel/core';\nimport type { AstroIntegrationLogger } from 'astro';\nimport type { ExtractedKeyOccurrence } from './types.js';\n\n/** TypeScript extensions (need 'typescript' babel plugin) */\nexport const TS_EXTENSIONS = ['.ts', '.tsx'] as const;\n\n/** JSX extensions (need 'jsx' babel plugin) */\nexport const JSX_EXTENSIONS = ['.jsx', '.tsx'] as const;\n\n/** Plain JS extensions */\nexport const JS_EXTENSIONS = ['.js', '.jsx'] as const;\n\n/**\n * Extensions that can be parsed directly by Babel (raw source files).\n * Used for eager scanning before Vite processes them.\n */\nexport const BABEL_EXTENSIONS = [...TS_EXTENSIONS, ...JS_EXTENSIONS] as const;\n\n/**\n * All extensions that may contain t() calls.\n * Includes .astro which requires Astro's compiler before Babel can parse it.\n */\nexport const ALL_EXTENSIONS = [...BABEL_EXTENSIONS, '.astro'] as const;\n\nexport type ExtractOptions = {\n filename: string;\n code: string;\n logger: AstroIntegrationLogger;\n stripFallbacks?: boolean | undefined;\n onKeyExtracted?: ((key: ExtractedKeyOccurrence) => void) | undefined;\n};\n\nexport type ExtractResult = {\n keys: ExtractedKeyOccurrence[];\n code: string | null;\n map: FileResult['map'];\n};\n\n/**\n * Extract keys from a single file using Babel.\n * Optionally strips fallbacks (for production builds).\n */\nexport async function extractKeysFromFile(options: ExtractOptions): Promise<ExtractResult> {\n const { filename, code, logger, stripFallbacks = false, onKeyExtracted } = options;\n\n const keys: ExtractedKeyOccurrence[] = [];\n\n // dynamic imports prevent Babel from being bundled into server runtime\n const [{ transformAsync }, { i18nExtractPlugin }] = await Promise.all([\n import('@babel/core'),\n import('./babel-plugin.js'),\n ]);\n\n // treat .astro files as TypeScript (otherwise type imports fail)\n const isTypeScript = TS_EXTENSIONS.some((ext) => filename.endsWith(ext)) || filename.endsWith('.astro');\n const isJSX = JSX_EXTENSIONS.some((ext) => filename.endsWith(ext));\n\n const result = await transformAsync(code, {\n filename,\n sourceMaps: stripFallbacks, // only need source maps when transforming\n babelrc: false,\n configFile: false,\n parserOpts: {\n plugins: [...(isTypeScript ? ['typescript' as const] : []), ...(isJSX ? ['jsx' as const] : [])],\n },\n plugins: [\n [\n i18nExtractPlugin,\n {\n stripFallbacks,\n onKeyExtracted: (key: ExtractedKeyOccurrence) => {\n keys.push(key);\n onKeyExtracted?.(key);\n },\n logger,\n },\n ] as PluginItem,\n ],\n });\n\n return {\n keys,\n code: result?.code ?? null,\n map: result?.map ?? null,\n };\n}\n","import type { AstroIntegrationLogger } from 'astro';\nimport type { ConsistencyCheckLevel } from '../integration/types.js';\nimport type { TranslationMeta } from '../shared/types.js';\nimport type { ExtractedKey, ExtractedKeyOccurrence } from './types.js';\n\n/**\n * Inconsistency between two occurrences of the same translation key\n */\nexport type KeyInconsistency = {\n key: string;\n field: 'fallback' | 'description' | 'variables';\n locations: [string, string];\n values: [string, string];\n};\n\n/**\n * Store for extracted i18n keys.\n * Stores occurrences and produces deduplicated keys with all file locations.\n */\nexport class KeyStore {\n /** All key occurrences (may have duplicates) */\n private readonly occurrences: ExtractedKeyOccurrence[] = [];\n\n /** Tracks reported inconsistencies to avoid duplicate warnings */\n private readonly reportedInconsistencies = new Set<string>();\n\n /** Whether an error-level inconsistency was found (for build failure) */\n private hasInconsistencyError = false;\n\n readonly fileToKeys = new Map<string, string[]>();\n readonly filesWithI18n = new Set<string>();\n\n constructor(\n private readonly logger: AstroIntegrationLogger,\n private readonly consistency: ConsistencyCheckLevel = 'error',\n ) {}\n\n /**\n * Add key occurrences for a file, replacing any existing occurrences for that file.\n */\n addFileKeys(filename: string, keys: ExtractedKeyOccurrence[]): void {\n const newKeyStrings = keys.map((k) => k.key);\n const oldKeys = this.fileToKeys.get(filename);\n\n // log key changes on HMR\n if (oldKeys) {\n const uniqueNew = [...new Set(newKeyStrings)];\n const uniqueOld = [...new Set(oldKeys)];\n const added = uniqueNew.filter((k) => !uniqueOld.includes(k));\n const removed = uniqueOld.filter((k) => !uniqueNew.includes(k));\n const file = filename.split('/').pop();\n\n if (added.length > 0 || removed.length > 0) {\n const parts: string[] = [];\n\n if (added.length > 0) parts.push(`+${added.length}`);\n if (removed.length > 0) parts.push(`-${removed.length}`);\n\n this.logger.info(`hmr: ${parts.join(' ')} key(s) in ${file}`);\n }\n }\n\n // remove old occurrences for this file (if any)\n if (oldKeys) {\n for (let i = this.occurrences.length - 1; i >= 0; i--) {\n if (this.occurrences[i]?.file === filename) {\n this.occurrences.splice(i, 1);\n }\n }\n }\n\n this.filesWithI18n.add(filename);\n\n // check for inconsistencies before adding new keys\n if (this.consistency !== 'off') {\n this.checkConsistency(keys);\n }\n\n if (keys.length > 0) {\n this.occurrences.push(...keys);\n }\n\n this.fileToKeys.set(filename, newKeyStrings);\n }\n\n /**\n * Check new keys for inconsistencies with existing occurrences.\n */\n private checkConsistency(newKeys: ExtractedKeyOccurrence[]): void {\n // build a map of existing keys (first occurrence for each key)\n const existingByKey = new Map<string, ExtractedKeyOccurrence>();\n\n for (const occ of this.occurrences) {\n if (!existingByKey.has(occ.key)) {\n existingByKey.set(occ.key, occ);\n }\n }\n\n for (const newKey of newKeys) {\n const existing = existingByKey.get(newKey.key);\n\n if (!existing) continue;\n\n const inconsistencies = this.findInconsistencies(newKey, existing);\n\n for (const inc of inconsistencies) {\n this.reportInconsistency(inc);\n }\n }\n }\n\n /**\n * Compare two occurrences and find any metadata inconsistencies.\n */\n private findInconsistencies(a: ExtractedKeyOccurrence, b: ExtractedKeyOccurrence): KeyInconsistency[] {\n const result: KeyInconsistency[] = [];\n const locA = `${a.file}:${a.line}`;\n const locB = `${b.file}:${b.line}`;\n\n // check fallback\n if (a.meta.fallback !== b.meta.fallback) {\n result.push({\n key: a.key,\n field: 'fallback',\n locations: [locA, locB],\n values: [a.meta.fallback, b.meta.fallback],\n });\n }\n\n // check description\n if (a.meta.description !== b.meta.description) {\n result.push({\n key: a.key,\n field: 'description',\n locations: [locA, locB],\n values: [a.meta.description ?? '(none)', b.meta.description ?? '(none)'],\n });\n }\n\n // check variables\n const varsA = this.serializeVariables(a.meta);\n const varsB = this.serializeVariables(b.meta);\n\n if (varsA !== varsB) {\n result.push({\n key: a.key,\n field: 'variables',\n locations: [locA, locB],\n values: [varsA || '(none)', varsB || '(none)'],\n });\n }\n\n return result;\n }\n\n /**\n * Serialize variables for comparison.\n */\n private serializeVariables(meta: TranslationMeta): string {\n if (!meta.variables) return '';\n\n // sort keys for consistent comparison\n const sorted: Record<string, unknown> = {};\n\n for (const key of Object.keys(meta.variables).sort()) {\n sorted[key] = meta.variables[key];\n }\n\n return JSON.stringify(sorted);\n }\n\n /**\n * Report an inconsistency via logger.\n */\n private reportInconsistency(inc: KeyInconsistency): void {\n // create a unique key for this inconsistency to avoid duplicate reports\n // use key+field only (not locations) to report once per inconsistent key\n const incKey = `${inc.key}:${inc.field}`;\n\n if (this.reportedInconsistencies.has(incKey)) return;\n\n this.reportedInconsistencies.add(incKey);\n\n const message =\n `inconsistent ${inc.field} for key \"${inc.key}\"\\n` +\n ` ${inc.locations[0]}: ${JSON.stringify(inc.values[0])}\\n` +\n ` ${inc.locations[1]}: ${JSON.stringify(inc.values[1])}`;\n\n if (this.consistency === 'error') {\n this.logger.error(message);\n this.hasInconsistencyError = true;\n } else {\n this.logger.warn(message);\n }\n }\n\n /**\n * Check if any error-level inconsistencies were found.\n * Call this at the end of build to determine if it should fail.\n */\n get hasErrors(): boolean {\n return this.hasInconsistencyError;\n }\n\n /**\n * Get deduplicated keys with all file locations merged.\n * Last occurrence's meta wins for each key.\n */\n get extractedKeys(): ExtractedKey[] {\n const keyMap = new Map<string, ExtractedKey>();\n\n for (const occurrence of this.occurrences) {\n const existing = keyMap.get(occurrence.key);\n const fileLocation = `${occurrence.file}:${occurrence.line}`;\n\n if (existing) {\n // add file location if not already present\n if (!existing.files.includes(fileLocation)) {\n existing.files.push(fileLocation);\n }\n\n // update meta (last one wins)\n existing.meta = occurrence.meta;\n } else {\n keyMap.set(occurrence.key, {\n key: occurrence.key,\n meta: occurrence.meta,\n files: [fileLocation],\n });\n }\n }\n\n return Array.from(keyMap.values());\n }\n\n /**\n * Get count of unique keys.\n */\n get uniqueKeyCount(): number {\n return new Set(this.occurrences.map((k) => k.key)).size;\n }\n\n /**\n * Merge another store's data into this one.\n */\n merge(other: KeyStore): void {\n for (const [file, keys] of other.fileToKeys) {\n this.fileToKeys.set(file, keys);\n }\n\n for (const file of other.filesWithI18n) {\n this.filesWithI18n.add(file);\n }\n\n // check for inconsistencies when merging\n if (this.consistency !== 'off') {\n this.checkConsistency(other.occurrences);\n }\n\n this.occurrences.push(...other.occurrences);\n }\n\n /**\n * Internal access to occurrences (for merge)\n */\n get occurrencesList(): ExtractedKeyOccurrence[] {\n return this.occurrences;\n }\n}\n","import fs from 'node:fs';\nimport path from 'node:path';\nimport type { AstroIntegrationLogger } from 'astro';\nimport type { ConsistencyCheckLevel } from '../integration/types.js';\nimport { ALL_EXTENSIONS, extractKeysFromFile } from './extract.js';\nimport { KeyStore } from './key-store.js';\n\nconst GLOB_PATTERN = `src/**/*.{${ALL_EXTENSIONS.map((e) => e.slice(1)).join(',')}}`;\n\n/**\n * Compile .astro file to JS using Astro's compiler.\n * Returns the compiled code or null if compilation fails.\n */\nasync function compileAstro(code: string, filename: string): Promise<string | null> {\n try {\n const { transform } = await import('@astrojs/compiler');\n const result = await transform(code, { filename });\n\n return result.code;\n } catch {\n return null;\n }\n}\n\nexport type ScanOptions = {\n projectRoot: string;\n logger: AstroIntegrationLogger;\n consistency: ConsistencyCheckLevel;\n};\n\n/**\n * Eagerly scan all source files for t() calls.\n * Used in dev mode to extract all keys upfront, instead of waiting for\n * Vite's lazy transform hook to process files on-demand.\n */\nexport async function scan(options: ScanOptions): Promise<KeyStore> {\n const { projectRoot, logger, consistency } = options;\n const store = new KeyStore(logger, consistency);\n\n const files: string[] = [];\n\n for await (const entry of fs.promises.glob(GLOB_PATTERN, {\n cwd: projectRoot,\n exclude: (name) => name === 'node_modules',\n })) {\n files.push(path.resolve(projectRoot, entry));\n }\n\n const results = await Promise.all(\n files.map(async (file) => {\n let code = await fs.promises.readFile(file, 'utf-8');\n\n // quick check: skip files without i18n translate import\n if (!code.includes('@astroscope/i18n/translate')) {\n return { file, keys: null };\n }\n\n // compile .astro files first\n if (file.endsWith('.astro')) {\n const compiled = await compileAstro(code, file);\n\n if (!compiled) {\n return { file, keys: null };\n }\n\n code = compiled;\n }\n\n // extract keys\n const result = await extractKeysFromFile({\n filename: file,\n code,\n logger,\n stripFallbacks: false,\n });\n\n return { file, keys: result.keys.length > 0 ? result.keys : null };\n }),\n );\n\n for (const { file, keys } of results) {\n if (keys) {\n store.addFileKeys(file, keys);\n }\n }\n\n return store;\n}\n","import fs from 'node:fs';\nimport path from 'node:path';\nimport type { AstroIntegrationLogger } from 'astro';\nimport MagicString from 'magic-string';\nimport type { Plugin } from 'vite';\nimport type { ConsistencyCheckLevel } from '../integration/types.js';\nimport { chunkIdToName } from '../shared/url.js';\nimport { ALL_EXTENSIONS, extractKeysFromFile } from './extract.js';\nimport { KeyStore } from './key-store.js';\nimport { getGlobalState, getManifest } from './manifest.js';\nimport { scan } from './scan.js';\n\nconst VIRTUAL_MODULE_ID = 'virtual:@astroscope/i18n/manifest';\nconst isVirtualModuleId = (id: string) => id === VIRTUAL_MODULE_ID || id.startsWith(`${VIRTUAL_MODULE_ID}?`);\n\nconst RESOLVED_VIRTUAL_MODULE_ID = `\\0${VIRTUAL_MODULE_ID}`;\nconst isResolvedVirtualModuleId = (id: string) =>\n id === RESOLVED_VIRTUAL_MODULE_ID || id.startsWith(`\\0${VIRTUAL_MODULE_ID}?`);\n\n// manifest JSON file name (emitted during build)\nconst MANIFEST_FILE_NAME = 'i18n-manifest.json';\n\nexport type I18nVitePluginOptions = {\n logger: AstroIntegrationLogger;\n consistency: ConsistencyCheckLevel;\n};\n\n/**\n * Vite plugin for i18n extraction, chunk mapping, and loader injection\n *\n * This plugin:\n * 1. Extracts all t() calls using Babel AST parsing\n * 2. Strips fallback arguments in production builds\n * 3. Maps translation keys to output chunks\n * 4. Generates a manifest for syncing with translation providers\n * 5. Injects translation loader into chunks that use t()\n */\nexport function i18nVitePlugin(options: I18nVitePluginOptions): Plugin {\n const { logger, consistency } = options;\n\n const store = new KeyStore(logger, consistency);\n\n // sync store's computed keys to global state (for dev mode live access)\n const syncGlobalState = () => {\n const state = getGlobalState();\n state.extractedKeys = store.extractedKeys;\n state.version++;\n };\n\n let isBuild = false;\n let projectRoot = '';\n\n return {\n name: '@astroscope/i18n/extract',\n enforce: 'pre',\n\n configResolved(config) {\n const state = getGlobalState();\n state.projectRoot = config.root;\n\n isBuild = config.command === 'build';\n projectRoot = config.root;\n },\n\n async configureServer(server) {\n // configureServer can be called during astro build's internal dev server\n // check isProduction to skip eager scanning during build\n if (server.config.isProduction || isBuild) {\n return;\n }\n\n // in dev mode, eagerly scan all files upfront\n // this ensures all t() calls are found immediately\n // (otherwise vite loads lazily as files are requested)\n const result = await scan({ projectRoot, logger, consistency });\n\n store.merge(result);\n\n syncGlobalState();\n\n logger.info(`dev mode: scanned ${result.fileToKeys.size} files, found ${result.uniqueKeyCount} keys`);\n },\n\n resolveId(id) {\n if (isVirtualModuleId(id)) {\n return `\\0${id}`;\n }\n },\n\n load(id) {\n if (isResolvedVirtualModuleId(id)) {\n if (isBuild) {\n // in build mode, load manifest from JSON file at runtime\n return `\nimport { readFileSync } from 'node:fs';\nimport { fileURLToPath } from 'node:url';\nimport { dirname, join } from 'node:path';\n\nconst __dirname = dirname(fileURLToPath(import.meta.url));\nconst manifestJson = JSON.parse(readFileSync(join(__dirname, '${MANIFEST_FILE_NAME}'), 'utf-8'));\n\nexport const manifest = manifestJson;\nexport function getManifest() { return manifestJson; }\n`;\n }\n\n // in dev mode, import the getManifest function from the plugin\n // this provides live access to the extraction state as files are transformed\n return `\nimport { getManifest as _getManifest } from '@astroscope/i18n/extraction';\nexport const manifest = { keys: [], chunks: {} };\nexport function getManifest() { return _getManifest(); }\n`;\n }\n },\n\n async transform(code, filename) {\n // only process included file types\n if (!ALL_EXTENSIONS.some((ext) => filename.endsWith(ext))) {\n return null;\n }\n\n // skip node_modules\n if (filename.includes('node_modules')) {\n return null;\n }\n\n // quick check: skip files without i18n translate import\n if (!code.includes('@astroscope/i18n/translate')) {\n return null;\n }\n\n const result = await extractKeysFromFile({\n filename,\n code,\n logger,\n stripFallbacks: isBuild,\n });\n\n store.addFileKeys(filename, result.keys);\n\n syncGlobalState();\n\n // build mode: return transformed code with source map\n // serialize babel's map to a string: vite accepts string source maps and\n // its structural ExistingRawSourceMap type (vite 8) rejects babel's map shape\n if (isBuild && result.code) {\n return result.map ? { code: result.code, map: JSON.stringify(result.map) } : { code: result.code };\n }\n\n return null; // dev mode: keep fallbacks\n },\n\n generateBundle(_, bundle) {\n const state = getGlobalState();\n\n // reset for each build (server + client run separately)\n state.chunkManifest = {};\n state.importsManifest = {};\n\n // first pass: collect keys, determine which chunks have i18n, and build direct imports map\n const chunksWithI18n = new Set<string>();\n const chunkNameToFileName = new Map<string, string>();\n const directImports = new Map<string, string[]>(); // chunk → direct imports (chunks only)\n\n for (const [fileName, chunk] of Object.entries(bundle)) {\n if (chunk.type === 'chunk' && chunk.moduleIds) {\n const keys = new Set<string>();\n let hasI18nModule = false;\n\n // collect keys from all (sub)modules in the chunk\n for (const moduleId of chunk.moduleIds) {\n const moduleKeys = store.fileToKeys.get(moduleId);\n\n moduleKeys?.forEach((k) => keys.add(k));\n\n if (store.filesWithI18n.has(moduleId)) {\n hasI18nModule = true;\n }\n }\n\n const chunkName = chunkIdToName(fileName.replace(/\\.js$/, '')); // cut .js extension\n\n if (keys.size) {\n state.chunkManifest[chunkName] = Array.from(keys);\n }\n\n if (hasI18nModule) {\n chunksWithI18n.add(chunkName);\n }\n\n chunkNameToFileName.set(chunkName, fileName);\n\n // store direct imports for flattening later\n const imports: string[] = [];\n\n for (const importedFile of chunk.imports) {\n const baseName = importedFile.replace(/^.*\\//, '').replace(/\\.js$/, '');\n const importedChunkName = chunkIdToName(baseName);\n\n imports.push(importedChunkName);\n }\n\n directImports.set(chunkName, imports);\n }\n }\n\n // build flattened imports manifest\n // tracks all direct and indirect imports that have i18n translations\n // circular dependency detection\n const flattenImports = (chunkName: string, visited: Set<string>): string[] => {\n if (visited.has(chunkName)) return []; // circular dependency, stop\n\n visited.add(chunkName);\n\n const result = new Set<string>();\n const imports = directImports.get(chunkName) ?? [];\n\n for (const imported of imports) {\n // add the imported chunk itself if it has i18n translations\n if (state.chunkManifest[imported]) {\n result.add(imported);\n }\n\n // recursively add all descendants with i18n\n for (const descendant of flattenImports(imported, visited)) {\n result.add(descendant);\n }\n }\n\n visited.delete(chunkName); // allow visiting from different paths\n\n return Array.from(result);\n };\n\n // compute flattened imports for ALL chunks (not just those with i18n)\n // because a chunk without i18n may import chunks that have i18n\n for (const chunkName of directImports.keys()) {\n const flattened = flattenImports(chunkName, new Set());\n\n if (flattened.length > 0) {\n state.importsManifest[chunkName] = flattened;\n }\n }\n\n // second pass: inject loaders (prefetch is handled by directives)\n for (const [fileName, chunk] of Object.entries(bundle)) {\n if (chunk.type !== 'chunk') continue;\n\n const chunkName = chunkIdToName(fileName.replace(/\\.js$/, ''));\n\n if (!chunksWithI18n.has(chunkName)) continue;\n\n // simple translation loader - just load own translations\n // prefetching is handled by directives using the imports manifest\n const loaderCode =\n `if(typeof window!=='undefined'&&window.__i18n__){` +\n `const _=window.__i18n__,h=_.hashes[${JSON.stringify(chunkName)}];` +\n `if(h)await import(\\`/_i18n/\\${_.locale}/${chunkName}.\\${h}.js\\`);` +\n `}`;\n\n // inject loader at start of chunk preserving source maps\n const s = new MagicString(chunk.code);\n\n s.prepend(loaderCode);\n chunk.code = s.toString();\n\n if (chunk.map) {\n chunk.map = s.generateMap({ hires: true }) as typeof chunk.map;\n }\n }\n\n logger.info(`manifest: ${Object.keys(state.chunkManifest).length} chunks, ${store.uniqueKeyCount} keys`);\n\n // fail build if consistency check is set to 'error' and inconsistencies were found\n if (consistency === 'error' && store.hasErrors) {\n throw new Error('i18n: build failed due to translation key inconsistencies');\n }\n },\n\n writeBundle(outputOptions) {\n // write the manifest JSON file after bundle is written\n // only emit from client build (has full chunk mapping, runs after server build)\n // write to server directory so it's not publicly accessible (same location as the virtual module)\n if (this.environment.name === 'client' && outputOptions.dir) {\n const chunksDir = path.resolve(outputOptions.dir, '..', 'server', 'chunks');\n const manifestPath = path.join(chunksDir, MANIFEST_FILE_NAME);\n\n if (fs.existsSync(chunksDir)) {\n fs.writeFileSync(manifestPath, JSON.stringify(getManifest()));\n } else {\n logger.error(`server chunks directory not found, cannot write manifest to ${manifestPath}`);\n }\n }\n },\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"vite-plugin-CXUN45oF.js","names":[],"sources":["../src/extraction/extract.ts","../src/extraction/key-store.ts","../src/extraction/scan.ts","../src/extraction/vite-plugin.ts"],"sourcesContent":["import type { FileResult, PluginItem } from '@babel/core';\nimport type { AstroIntegrationLogger } from 'astro';\nimport type { ExtractedKeyOccurrence } from './types.js';\n\n/** TypeScript extensions (need 'typescript' babel plugin) */\nexport const TS_EXTENSIONS = ['.ts', '.tsx'] as const;\n\n/** JSX extensions (need 'jsx' babel plugin) */\nexport const JSX_EXTENSIONS = ['.jsx', '.tsx'] as const;\n\n/** Plain JS extensions */\nexport const JS_EXTENSIONS = ['.js', '.jsx'] as const;\n\n/**\n * Extensions that can be parsed directly by Babel (raw source files).\n * Used for eager scanning before Vite processes them.\n */\nexport const BABEL_EXTENSIONS = [...TS_EXTENSIONS, ...JS_EXTENSIONS] as const;\n\n/**\n * All extensions that may contain t() calls.\n * Includes .astro which requires Astro's compiler before Babel can parse it.\n */\nexport const ALL_EXTENSIONS = [...BABEL_EXTENSIONS, '.astro'] as const;\n\nexport type ExtractOptions = {\n filename: string;\n code: string;\n logger: AstroIntegrationLogger;\n stripFallbacks?: boolean | undefined;\n onKeyExtracted?: ((key: ExtractedKeyOccurrence) => void) | undefined;\n};\n\nexport type ExtractResult = {\n keys: ExtractedKeyOccurrence[];\n code: string | null;\n map: FileResult['map'];\n};\n\n/**\n * Extract keys from a single file using Babel.\n * Optionally strips fallbacks (for production builds).\n */\nexport async function extractKeysFromFile(options: ExtractOptions): Promise<ExtractResult> {\n const { filename, code, logger, stripFallbacks = false, onKeyExtracted } = options;\n\n const keys: ExtractedKeyOccurrence[] = [];\n\n // dynamic imports prevent Babel from being bundled into server runtime\n const [{ transformAsync }, { i18nExtractPlugin }] = await Promise.all([\n import('@babel/core'),\n import('./babel-plugin.js'),\n ]);\n\n // treat .astro files as TypeScript (otherwise type imports fail)\n const isTypeScript = TS_EXTENSIONS.some((ext) => filename.endsWith(ext)) || filename.endsWith('.astro');\n const isJSX = JSX_EXTENSIONS.some((ext) => filename.endsWith(ext));\n\n const result = await transformAsync(code, {\n filename,\n sourceMaps: stripFallbacks, // only need source maps when transforming\n babelrc: false,\n configFile: false,\n parserOpts: {\n plugins: [...(isTypeScript ? ['typescript' as const] : []), ...(isJSX ? ['jsx' as const] : [])],\n },\n plugins: [\n [\n i18nExtractPlugin,\n {\n stripFallbacks,\n onKeyExtracted: (key: ExtractedKeyOccurrence) => {\n keys.push(key);\n onKeyExtracted?.(key);\n },\n logger,\n },\n ] as PluginItem,\n ],\n });\n\n return {\n keys,\n code: result?.code ?? null,\n map: result?.map ?? null,\n };\n}\n","import type { AstroIntegrationLogger } from 'astro';\nimport type { ConsistencyCheckLevel } from '../integration/types.js';\nimport type { TranslationMeta } from '../shared/types.js';\nimport type { ExtractedKey, ExtractedKeyOccurrence } from './types.js';\n\n/**\n * Inconsistency between two occurrences of the same translation key\n */\nexport type KeyInconsistency = {\n key: string;\n field: 'fallback' | 'description' | 'variables';\n locations: [string, string];\n values: [string, string];\n};\n\n/**\n * Store for extracted i18n keys.\n * Stores occurrences and produces deduplicated keys with all file locations.\n */\nexport class KeyStore {\n /** All key occurrences (may have duplicates) */\n private readonly occurrences: ExtractedKeyOccurrence[] = [];\n\n /** Tracks reported inconsistencies to avoid duplicate warnings */\n private readonly reportedInconsistencies = new Set<string>();\n\n /** Whether an error-level inconsistency was found (for build failure) */\n private hasInconsistencyError = false;\n\n readonly fileToKeys = new Map<string, string[]>();\n readonly filesWithI18n = new Set<string>();\n\n constructor(\n private readonly logger: AstroIntegrationLogger,\n private readonly consistency: ConsistencyCheckLevel = 'error',\n ) {}\n\n /**\n * Add key occurrences for a file, replacing any existing occurrences for that file.\n */\n addFileKeys(filename: string, keys: ExtractedKeyOccurrence[]): void {\n const newKeyStrings = keys.map((k) => k.key);\n const oldKeys = this.fileToKeys.get(filename);\n\n // log key changes on HMR\n if (oldKeys) {\n const uniqueNew = [...new Set(newKeyStrings)];\n const uniqueOld = [...new Set(oldKeys)];\n const added = uniqueNew.filter((k) => !uniqueOld.includes(k));\n const removed = uniqueOld.filter((k) => !uniqueNew.includes(k));\n const file = filename.split('/').pop();\n\n if (added.length > 0 || removed.length > 0) {\n const parts: string[] = [];\n\n if (added.length > 0) parts.push(`+${added.length}`);\n if (removed.length > 0) parts.push(`-${removed.length}`);\n\n this.logger.info(`hmr: ${parts.join(' ')} key(s) in ${file}`);\n }\n }\n\n // remove old occurrences for this file (if any)\n if (oldKeys) {\n for (let i = this.occurrences.length - 1; i >= 0; i--) {\n if (this.occurrences[i]?.file === filename) {\n this.occurrences.splice(i, 1);\n }\n }\n }\n\n this.filesWithI18n.add(filename);\n\n // check for inconsistencies before adding new keys\n if (this.consistency !== 'off') {\n this.checkConsistency(keys);\n }\n\n if (keys.length > 0) {\n this.occurrences.push(...keys);\n }\n\n this.fileToKeys.set(filename, newKeyStrings);\n }\n\n /**\n * Check new keys for inconsistencies with existing occurrences.\n */\n private checkConsistency(newKeys: ExtractedKeyOccurrence[]): void {\n // build a map of existing keys (first occurrence for each key)\n const existingByKey = new Map<string, ExtractedKeyOccurrence>();\n\n for (const occ of this.occurrences) {\n if (!existingByKey.has(occ.key)) {\n existingByKey.set(occ.key, occ);\n }\n }\n\n for (const newKey of newKeys) {\n const existing = existingByKey.get(newKey.key);\n\n if (!existing) continue;\n\n const inconsistencies = this.findInconsistencies(newKey, existing);\n\n for (const inc of inconsistencies) {\n this.reportInconsistency(inc);\n }\n }\n }\n\n /**\n * Compare two occurrences and find any metadata inconsistencies.\n */\n private findInconsistencies(a: ExtractedKeyOccurrence, b: ExtractedKeyOccurrence): KeyInconsistency[] {\n const result: KeyInconsistency[] = [];\n const locA = `${a.file}:${a.line}`;\n const locB = `${b.file}:${b.line}`;\n\n // check fallback\n if (a.meta.fallback !== b.meta.fallback) {\n result.push({\n key: a.key,\n field: 'fallback',\n locations: [locA, locB],\n values: [a.meta.fallback, b.meta.fallback],\n });\n }\n\n // check description\n if (a.meta.description !== b.meta.description) {\n result.push({\n key: a.key,\n field: 'description',\n locations: [locA, locB],\n values: [a.meta.description ?? '(none)', b.meta.description ?? '(none)'],\n });\n }\n\n // check variables\n const varsA = this.serializeVariables(a.meta);\n const varsB = this.serializeVariables(b.meta);\n\n if (varsA !== varsB) {\n result.push({\n key: a.key,\n field: 'variables',\n locations: [locA, locB],\n values: [varsA || '(none)', varsB || '(none)'],\n });\n }\n\n return result;\n }\n\n /**\n * Serialize variables for comparison.\n */\n private serializeVariables(meta: TranslationMeta): string {\n if (!meta.variables) return '';\n\n // sort keys for consistent comparison\n const sorted: Record<string, unknown> = {};\n\n for (const key of Object.keys(meta.variables).sort()) {\n sorted[key] = meta.variables[key];\n }\n\n return JSON.stringify(sorted);\n }\n\n /**\n * Report an inconsistency via logger.\n */\n private reportInconsistency(inc: KeyInconsistency): void {\n // create a unique key for this inconsistency to avoid duplicate reports\n // use key+field only (not locations) to report once per inconsistent key\n const incKey = `${inc.key}:${inc.field}`;\n\n if (this.reportedInconsistencies.has(incKey)) return;\n\n this.reportedInconsistencies.add(incKey);\n\n const message =\n `inconsistent ${inc.field} for key \"${inc.key}\"\\n` +\n ` ${inc.locations[0]}: ${JSON.stringify(inc.values[0])}\\n` +\n ` ${inc.locations[1]}: ${JSON.stringify(inc.values[1])}`;\n\n if (this.consistency === 'error') {\n this.logger.error(message);\n this.hasInconsistencyError = true;\n } else {\n this.logger.warn(message);\n }\n }\n\n /**\n * Check if any error-level inconsistencies were found.\n * Call this at the end of build to determine if it should fail.\n */\n get hasErrors(): boolean {\n return this.hasInconsistencyError;\n }\n\n /**\n * Get deduplicated keys with all file locations merged.\n * Last occurrence's meta wins for each key.\n */\n get extractedKeys(): ExtractedKey[] {\n const keyMap = new Map<string, ExtractedKey>();\n\n for (const occurrence of this.occurrences) {\n const existing = keyMap.get(occurrence.key);\n const fileLocation = `${occurrence.file}:${occurrence.line}`;\n\n if (existing) {\n // add file location if not already present\n if (!existing.files.includes(fileLocation)) {\n existing.files.push(fileLocation);\n }\n\n // update meta (last one wins)\n existing.meta = occurrence.meta;\n } else {\n keyMap.set(occurrence.key, {\n key: occurrence.key,\n meta: occurrence.meta,\n files: [fileLocation],\n });\n }\n }\n\n return Array.from(keyMap.values());\n }\n\n /**\n * Get count of unique keys.\n */\n get uniqueKeyCount(): number {\n return new Set(this.occurrences.map((k) => k.key)).size;\n }\n\n /**\n * Merge another store's data into this one.\n */\n merge(other: KeyStore): void {\n for (const [file, keys] of other.fileToKeys) {\n this.fileToKeys.set(file, keys);\n }\n\n for (const file of other.filesWithI18n) {\n this.filesWithI18n.add(file);\n }\n\n // check for inconsistencies when merging\n if (this.consistency !== 'off') {\n this.checkConsistency(other.occurrences);\n }\n\n this.occurrences.push(...other.occurrences);\n }\n\n /**\n * Internal access to occurrences (for merge)\n */\n get occurrencesList(): ExtractedKeyOccurrence[] {\n return this.occurrences;\n }\n}\n","import fs from 'node:fs';\nimport path from 'node:path';\nimport type { AstroIntegrationLogger } from 'astro';\nimport type { ConsistencyCheckLevel } from '../integration/types.js';\nimport { ALL_EXTENSIONS, extractKeysFromFile } from './extract.js';\nimport { KeyStore } from './key-store.js';\n\nconst GLOB_PATTERN = `src/**/*.{${ALL_EXTENSIONS.map((e) => e.slice(1)).join(',')}}`;\n\n/**\n * Compile .astro file to JS using Astro's compiler.\n * Returns the compiled code or null if compilation fails.\n */\nasync function compileAstro(code: string, filename: string): Promise<string | null> {\n try {\n const { transform } = await import('@astrojs/compiler');\n const result = await transform(code, { filename });\n\n return result.code;\n } catch {\n return null;\n }\n}\n\nexport type ScanOptions = {\n projectRoot: string;\n logger: AstroIntegrationLogger;\n consistency: ConsistencyCheckLevel;\n};\n\n/**\n * Eagerly scan all source files for t() calls.\n * Used in dev mode to extract all keys upfront, instead of waiting for\n * Vite's lazy transform hook to process files on-demand.\n */\nexport async function scan(options: ScanOptions): Promise<KeyStore> {\n const { projectRoot, logger, consistency } = options;\n const store = new KeyStore(logger, consistency);\n\n const files: string[] = [];\n\n for await (const entry of fs.promises.glob(GLOB_PATTERN, {\n cwd: projectRoot,\n exclude: (name) => name === 'node_modules',\n })) {\n files.push(path.resolve(projectRoot, entry));\n }\n\n const results = await Promise.all(\n files.map(async (file) => {\n let code = await fs.promises.readFile(file, 'utf-8');\n\n // quick check: skip files without i18n translate import\n if (!code.includes('@astroscope/i18n/translate')) {\n return { file, keys: null };\n }\n\n // compile .astro files first\n if (file.endsWith('.astro')) {\n const compiled = await compileAstro(code, file);\n\n if (!compiled) {\n return { file, keys: null };\n }\n\n code = compiled;\n }\n\n // extract keys\n const result = await extractKeysFromFile({\n filename: file,\n code,\n logger,\n stripFallbacks: false,\n });\n\n return { file, keys: result.keys.length > 0 ? result.keys : null };\n }),\n );\n\n for (const { file, keys } of results) {\n if (keys) {\n store.addFileKeys(file, keys);\n }\n }\n\n return store;\n}\n","import fs from 'node:fs';\nimport path from 'node:path';\nimport type { AstroIntegrationLogger } from 'astro';\nimport MagicString from 'magic-string';\nimport type { Plugin } from 'vite';\nimport type { ConsistencyCheckLevel } from '../integration/types.js';\nimport { chunkIdToName } from '../shared/url.js';\nimport { ALL_EXTENSIONS, extractKeysFromFile } from './extract.js';\nimport { KeyStore } from './key-store.js';\nimport { getGlobalState, getManifest } from './manifest.js';\nimport { scan } from './scan.js';\n\nconst VIRTUAL_MODULE_ID = 'virtual:@astroscope/i18n/manifest';\nconst isVirtualModuleId = (id: string) => id === VIRTUAL_MODULE_ID || id.startsWith(`${VIRTUAL_MODULE_ID}?`);\n\nconst RESOLVED_VIRTUAL_MODULE_ID = `\\0${VIRTUAL_MODULE_ID}`;\nconst isResolvedVirtualModuleId = (id: string) =>\n id === RESOLVED_VIRTUAL_MODULE_ID || id.startsWith(`\\0${VIRTUAL_MODULE_ID}?`);\n\n// manifest JSON file name (emitted during build)\nconst MANIFEST_FILE_NAME = 'i18n-manifest.json';\n\nexport type I18nVitePluginOptions = {\n logger: AstroIntegrationLogger;\n consistency: ConsistencyCheckLevel;\n};\n\n/**\n * Vite plugin for i18n extraction, chunk mapping, and loader injection\n *\n * This plugin:\n * 1. Extracts all t() calls using Babel AST parsing\n * 2. Strips fallback arguments in production builds\n * 3. Maps translation keys to output chunks\n * 4. Generates a manifest for syncing with translation providers\n * 5. Injects translation loader into chunks that use t()\n */\nexport function i18nVitePlugin(options: I18nVitePluginOptions): Plugin {\n const { logger, consistency } = options;\n\n const store = new KeyStore(logger, consistency);\n\n // sync store's computed keys to global state (for dev mode live access)\n const syncGlobalState = () => {\n const state = getGlobalState();\n state.extractedKeys = store.extractedKeys;\n state.version++;\n };\n\n let isBuild = false;\n let projectRoot = '';\n\n return {\n name: '@astroscope/i18n/extract',\n enforce: 'pre',\n\n configResolved(config) {\n const state = getGlobalState();\n state.projectRoot = config.root;\n\n isBuild = config.command === 'build';\n projectRoot = config.root;\n },\n\n async configureServer(server) {\n // configureServer can be called during astro build's internal dev server\n // check isProduction to skip eager scanning during build\n if (server.config.isProduction || isBuild) {\n return;\n }\n\n // in dev mode, eagerly scan all files upfront\n // this ensures all t() calls are found immediately\n // (otherwise vite loads lazily as files are requested)\n const result = await scan({ projectRoot, logger, consistency });\n\n store.merge(result);\n\n syncGlobalState();\n\n logger.info(`dev mode: scanned ${result.fileToKeys.size} files, found ${result.uniqueKeyCount} keys`);\n },\n\n resolveId(id) {\n if (isVirtualModuleId(id)) {\n return `\\0${id}`;\n }\n },\n\n load(id) {\n if (isResolvedVirtualModuleId(id)) {\n if (isBuild) {\n // in build mode, load manifest from JSON file at runtime\n return `\nimport { readFileSync } from 'node:fs';\nimport { fileURLToPath } from 'node:url';\nimport { dirname, join } from 'node:path';\n\nconst __dirname = dirname(fileURLToPath(import.meta.url));\nconst manifestJson = JSON.parse(readFileSync(join(__dirname, '${MANIFEST_FILE_NAME}'), 'utf-8'));\n\nexport const manifest = manifestJson;\nexport function getManifest() { return manifestJson; }\n`;\n }\n\n // in dev mode, import the getManifest function from the plugin\n // this provides live access to the extraction state as files are transformed\n return `\nimport { getManifest as _getManifest } from '@astroscope/i18n/extraction';\nexport const manifest = { keys: [], chunks: {} };\nexport function getManifest() { return _getManifest(); }\n`;\n }\n },\n\n async transform(code, filename) {\n // only process included file types\n if (!ALL_EXTENSIONS.some((ext) => filename.endsWith(ext))) {\n return null;\n }\n\n // skip node_modules\n if (filename.includes('node_modules')) {\n return null;\n }\n\n // quick check: skip files without i18n translate import\n if (!code.includes('@astroscope/i18n/translate')) {\n return null;\n }\n\n const result = await extractKeysFromFile({\n filename,\n code,\n logger,\n stripFallbacks: isBuild,\n });\n\n store.addFileKeys(filename, result.keys);\n\n syncGlobalState();\n\n // build mode: return transformed code with source map\n // serialize babel's map to a string: vite accepts string source maps and\n // its structural ExistingRawSourceMap type (vite 8) rejects babel's map shape\n if (isBuild && result.code) {\n return result.map ? { code: result.code, map: JSON.stringify(result.map) } : { code: result.code };\n }\n\n return null; // dev mode: keep fallbacks\n },\n\n generateBundle(_, bundle) {\n const state = getGlobalState();\n\n // reset for each build (server + client run separately)\n state.chunkManifest = {};\n state.importsManifest = {};\n\n // first pass: collect keys, determine which chunks have i18n, and build direct imports map\n const chunksWithI18n = new Set<string>();\n const chunkNameToFileName = new Map<string, string>();\n const directImports = new Map<string, string[]>(); // chunk → direct imports (chunks only)\n\n for (const [fileName, chunk] of Object.entries(bundle)) {\n if (chunk.type === 'chunk' && chunk.moduleIds) {\n const keys = new Set<string>();\n let hasI18nModule = false;\n\n // collect keys from all (sub)modules in the chunk\n for (const moduleId of chunk.moduleIds) {\n const moduleKeys = store.fileToKeys.get(moduleId);\n\n moduleKeys?.forEach((k) => keys.add(k));\n\n if (store.filesWithI18n.has(moduleId)) {\n hasI18nModule = true;\n }\n }\n\n const chunkName = chunkIdToName(fileName.replace(/\\.js$/, '')); // cut .js extension\n\n if (keys.size) {\n state.chunkManifest[chunkName] = Array.from(keys);\n }\n\n if (hasI18nModule) {\n chunksWithI18n.add(chunkName);\n }\n\n chunkNameToFileName.set(chunkName, fileName);\n\n // store direct imports for flattening later\n const imports: string[] = [];\n\n for (const importedFile of chunk.imports) {\n const baseName = importedFile.replace(/^.*\\//, '').replace(/\\.js$/, '');\n const importedChunkName = chunkIdToName(baseName);\n\n imports.push(importedChunkName);\n }\n\n directImports.set(chunkName, imports);\n }\n }\n\n // build flattened imports manifest\n // tracks all direct and indirect imports that have i18n translations\n // circular dependency detection\n const flattenImports = (chunkName: string, visited: Set<string>): string[] => {\n if (visited.has(chunkName)) return []; // circular dependency, stop\n\n visited.add(chunkName);\n\n const result = new Set<string>();\n const imports = directImports.get(chunkName) ?? [];\n\n for (const imported of imports) {\n // add the imported chunk itself if it has i18n translations\n if (state.chunkManifest[imported]) {\n result.add(imported);\n }\n\n // recursively add all descendants with i18n\n for (const descendant of flattenImports(imported, visited)) {\n result.add(descendant);\n }\n }\n\n visited.delete(chunkName); // allow visiting from different paths\n\n return Array.from(result);\n };\n\n // compute flattened imports for ALL chunks (not just those with i18n)\n // because a chunk without i18n may import chunks that have i18n\n for (const chunkName of directImports.keys()) {\n const flattened = flattenImports(chunkName, new Set());\n\n if (flattened.length > 0) {\n state.importsManifest[chunkName] = flattened;\n }\n }\n\n // second pass: inject loaders (prefetch is handled by directives)\n for (const [fileName, chunk] of Object.entries(bundle)) {\n if (chunk.type !== 'chunk') continue;\n\n const chunkName = chunkIdToName(fileName.replace(/\\.js$/, ''));\n\n if (!chunksWithI18n.has(chunkName)) continue;\n\n // simple translation loader - just load own translations\n // prefetching is handled by directives using the imports manifest\n const loaderCode =\n `if(typeof window!=='undefined'&&window.__i18n__){` +\n `const _=window.__i18n__,h=_.hashes[${JSON.stringify(chunkName)}];` +\n `if(h)await import(\\`/_i18n/\\${_.locale}/${chunkName}.\\${h}.js\\`);` +\n `}`;\n\n // inject loader at start of chunk preserving source maps\n const s = new MagicString(chunk.code);\n\n s.prepend(loaderCode);\n chunk.code = s.toString();\n\n if (chunk.map) {\n chunk.map = s.generateMap({ hires: true }) as typeof chunk.map;\n }\n }\n\n logger.info(`manifest: ${Object.keys(state.chunkManifest).length} chunks, ${store.uniqueKeyCount} keys`);\n\n // fail build if consistency check is set to 'error' and inconsistencies were found\n if (consistency === 'error' && store.hasErrors) {\n throw new Error('i18n: build failed due to translation key inconsistencies');\n }\n },\n\n writeBundle(outputOptions) {\n // write the manifest JSON file after bundle is written\n // only emit from client build (has full chunk mapping, runs after server build)\n // write to server directory so it's not publicly accessible (same location as the virtual module)\n if (this.environment.name === 'client' && outputOptions.dir) {\n const chunksDir = path.resolve(outputOptions.dir, '..', 'server', 'chunks');\n const manifestPath = path.join(chunksDir, MANIFEST_FILE_NAME);\n\n if (fs.existsSync(chunksDir)) {\n fs.writeFileSync(manifestPath, JSON.stringify(getManifest()));\n } else {\n logger.error(`server chunks directory not found, cannot write manifest to ${manifestPath}`);\n }\n }\n },\n };\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@astroscope/i18n",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "i18n for Astro + React islands — MessageFormat 2, automatic tree-shaking, parallel loading, any translation source",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -95,12 +95,12 @@
|
|
|
95
95
|
"tsdown": "^0.22.3",
|
|
96
96
|
"typescript": "^6.0.3",
|
|
97
97
|
"vite": "^8.1.0",
|
|
98
|
-
"@astroscope/
|
|
98
|
+
"@astroscope/node": "^1.2.1"
|
|
99
99
|
},
|
|
100
100
|
"peerDependencies": {
|
|
101
101
|
"astro": "^7.0.0",
|
|
102
102
|
"vite": "^8.0.0",
|
|
103
|
-
"@astroscope/
|
|
103
|
+
"@astroscope/node": "^1.2.1"
|
|
104
104
|
},
|
|
105
105
|
"scripts": {
|
|
106
106
|
"build": "tsdown src/index.ts src/internal.ts src/extraction/index.ts src/server/translate.ts src/client/translate.ts src/client/directives/load.ts src/client/directives/idle.ts src/client/directives/visible.ts src/client/directives/media.ts src/client/directives/only.ts src/client/directives/utils.ts --format esm --dts --no-fixed-extension --deps.never-bundle astro --deps.never-bundle astro/client/load.js --deps.never-bundle astro/client/idle.js --deps.never-bundle astro/client/visible.js --deps.never-bundle astro/client/media.js --deps.never-bundle astro/client/only.js --deps.never-bundle virtual:@astroscope/i18n/manifest --deps.never-bundle @babel/core",
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"context-DT91WgEO.js","names":[],"sources":["../src/extraction/hash.ts","../src/server/utils.ts","../src/server/i18n.ts","../src/server/context.ts"],"sourcesContent":["import { createHash } from 'node:crypto';\nimport type { RawTranslations } from '../shared/types.js';\nimport type { ChunkManifest } from './types.js';\n\n/**\n * Hash a string to a short hash (for cache busting)\n */\nfunction hashString(str: string): string {\n return createHash('sha256').update(str).digest('hex').substring(0, 8);\n}\n\n/**\n * Compute hash for chunk translations\n */\nexport function computeChunkHash(translations: RawTranslations, keys: string[]): string {\n const relevantTranslations: RawTranslations = {};\n\n for (const key of keys.sort()) {\n if (translations[key]) {\n relevantTranslations[key] = translations[key];\n }\n }\n\n return hashString(JSON.stringify(relevantTranslations));\n}\n\n/**\n * Compute hashes for all chunks in manifest\n */\nexport function computeAllChunkHashes(translations: RawTranslations, manifest: ChunkManifest): Record<string, string> {\n const hashes: Record<string, string> = {};\n\n for (const [chunkId, keys] of Object.entries(manifest)) {\n hashes[chunkId] = computeChunkHash(translations, keys);\n }\n\n return hashes;\n}\n","const LOWERCASE_A = 97;\n\n/**\n * Convert index to lowercase letter: 0→'a', 1→'b', ..., 25→'z'\n */\nfunction indexToLetter(index: number): string {\n return String.fromCharCode(LOWERCASE_A + index);\n}\n\n/**\n * Generate short variable name using bijective base-26: 0→'a', 25→'z', 26→'aa', 27→'ab', ...\n */\nexport function generateBB26(index: number): string {\n let name = '';\n let remaining = index;\n\n do {\n name = indexToLetter(remaining % 26) + name;\n remaining = Math.floor(remaining / 26) - 1;\n } while (remaining >= 0);\n\n return name;\n}\n","import { computeAllChunkHashes } from '../extraction/hash.js';\nimport { getGlobalState } from '../extraction/manifest.js';\nimport type { ExtractionManifest } from '../extraction/types.js';\nimport { compileTranslations } from '../shared/compiler.js';\nimport type { CompiledTranslations, RawTranslations } from '../shared/types.js';\nimport type { FallbackBehavior } from './types.js';\nimport { generateBB26 } from './utils.js';\n\nexport type I18nConfig = {\n locales: string[];\n defaultLocale?: string | undefined;\n fallback?: FallbackBehavior | undefined;\n};\n\ntype NormalizedConfig = {\n locales: string[];\n defaultLocale: string;\n fallback: FallbackBehavior;\n};\n\nclass I18nSingleton {\n // normalized user config with defaults applied\n private config: NormalizedConfig | null = null;\n\n // locale -> raw translation strings (before ICU compilation)\n private rawCache = new Map<string, RawTranslations>();\n\n // locale -> raw translations merged with manifest fallbacks\n private mergedCache = new Map<string, RawTranslations>();\n\n // locale -> compiled ICU MessageFormat functions\n private compiledCache = new Map<string, CompiledTranslations>();\n\n // locale -> chunk name -> content hash (for cache busting)\n private hashCache = new Map<string, Record<string, string>>();\n\n // locale -> inline script for I18nScript component\n private scriptCache = new Map<string, string>();\n\n // \"locale:chunkName\" -> encoded chunk response body\n private chunkCache = new Map<string, Uint8Array>();\n\n // manifest getter (set by init module, provides live data in dev mode)\n private manifestGetter: (() => ExtractionManifest) | null = null;\n\n // tracks the manifest version to detect when derived caches need invalidation\n private manifestVersion = 0;\n\n async configure(config: I18nConfig): Promise<void> {\n if (!config.locales?.length) {\n throw new Error('i18n.configure(): locales array is required and must not be empty');\n }\n\n for (const locale of config.locales) {\n if (!locale) {\n throw new Error('i18n.configure(): locale cannot be empty');\n }\n\n if (locale !== locale.trim()) {\n throw new Error(`i18n.configure(): locale \"${locale}\" has leading or trailing whitespace`);\n }\n }\n\n if (new Set(config.locales).size !== config.locales.length) {\n throw new Error('i18n.configure(): locales array contains duplicates');\n }\n\n if (config.defaultLocale !== undefined) {\n if (!config.defaultLocale) {\n throw new Error('i18n.configure(): defaultLocale cannot be empty');\n }\n\n if (config.defaultLocale !== config.defaultLocale.trim()) {\n throw new Error(`i18n.configure(): defaultLocale \"${config.defaultLocale}\" has leading or trailing whitespace`);\n }\n\n if (!config.locales.includes(config.defaultLocale)) {\n throw new Error(`i18n.configure(): defaultLocale \"${config.defaultLocale}\" is not in locales array`);\n }\n }\n\n this.config = {\n locales: config.locales,\n defaultLocale: config.defaultLocale ?? config.locales[0]!,\n fallback: config.fallback ?? 'fallback',\n };\n\n // dynamically import manifest getter from virtual module\n // this removes the need for a separate `import '@astroscope/i18n/init'`\n if (!this.manifestGetter) {\n const m = await import('virtual:@astroscope/i18n/manifest');\n this.manifestGetter = m.getManifest;\n }\n }\n\n isConfigured(): boolean {\n return this.config !== null;\n }\n\n getConfig(): NormalizedConfig {\n if (!this.config) {\n throw new Error('i18n not configured. Call i18n.configure() first.');\n }\n\n return this.config;\n }\n\n setTranslations(locale: string, raw: RawTranslations): void {\n this.rawCache.set(locale, raw);\n this.mergedCache.delete(locale); // invalidate merged cache\n this.compiledCache.delete(locale); // invalidate compiled cache\n this.hashCache.delete(locale); // invalidate hash cache\n this.scriptCache.delete(locale); // invalidate script cache\n\n // invalidate chunk cache entries for this locale\n for (const key of this.chunkCache.keys()) {\n if (key.startsWith(`${locale}:`)) {\n this.chunkCache.delete(key);\n }\n }\n\n // recompute hashes if chunk manifest exists (production only)\n const { chunks } = this.getManifest();\n\n if (Object.keys(chunks).length > 0) {\n this.hashCache.set(locale, computeAllChunkHashes(raw, chunks));\n }\n }\n\n getTranslations(locale: string): RawTranslations {\n this.invalidateIfManifestChanged();\n\n const cached = this.mergedCache.get(locale);\n\n if (cached) {\n return cached;\n }\n\n const raw = this.rawCache.get(locale) ?? {};\n const manifest = this.manifestGetter?.() ?? { keys: [], chunks: {}, imports: {} };\n\n if (manifest.keys.length === 0) {\n return raw;\n }\n\n // merge fallbacks for keys that don't have translations\n const withFallbacks: RawTranslations = { ...raw };\n\n for (const { key, meta } of manifest.keys) {\n if (!(key in withFallbacks) && meta.fallback) {\n withFallbacks[key] = meta.fallback;\n }\n }\n\n this.mergedCache.set(locale, withFallbacks);\n\n return withFallbacks;\n }\n\n getCompiledTranslations(locale: string): CompiledTranslations {\n const cached = this.compiledCache.get(locale);\n\n if (cached) {\n return cached;\n }\n\n const compiled = compileTranslations(locale, this.getTranslations(locale));\n\n this.compiledCache.set(locale, compiled);\n\n return compiled;\n }\n\n /**\n * Get the extraction manifest (keys + chunks).\n * - keys: translation keys extracted from codebase with metadata\n * - chunks: mapping of chunk names to their translation keys\n */\n getManifest(): ExtractionManifest {\n if (!this.manifestGetter) {\n throw new Error('i18n manifest not initialized. Call i18n.configure() first.');\n }\n\n return this.manifestGetter();\n }\n\n getHashes(locale: string): Record<string, string> {\n return this.hashCache.get(locale) ?? {};\n }\n\n /**\n * Get the encoded chunk response body for a locale/chunk combination.\n * Cached to avoid repeated JSON.stringify and encoding on each request.\n * Returns undefined if chunk not found in manifest.\n */\n getChunkBody(locale: string, chunkName: string): Uint8Array | undefined {\n const keys = this.getManifest().chunks[chunkName];\n\n if (!keys) {\n return undefined;\n }\n\n const cacheKey = `${locale}:${chunkName}`;\n const cached = this.chunkCache.get(cacheKey);\n\n if (cached) {\n return cached;\n }\n\n const translations = this.getTranslations(locale);\n const chunkTranslations: RawTranslations = {};\n\n for (const key of keys) {\n if (translations[key]) {\n chunkTranslations[key] = translations[key];\n }\n }\n\n const js = `/* @astroscope/i18n chunk: ${chunkName} */\n(function() {\n var i = window.__i18n__;\n if (i) Object.assign(i.translations, ${JSON.stringify(chunkTranslations)});\n})();\n`;\n\n const body = new TextEncoder().encode(js);\n\n this.chunkCache.set(cacheKey, body);\n\n return body;\n }\n\n /**\n * Get the inline script for I18nScript component.\n * Cached per locale, invalidated when translations change.\n */\n getClientScript(locale: string): string {\n const cached = this.scriptCache.get(locale);\n\n if (cached) {\n return cached;\n }\n\n const script = this.createClientScript(locale);\n\n this.scriptCache.set(locale, script);\n\n return script;\n }\n\n private createClientScript(locale: string): string {\n const { chunks, imports } = this.getManifest();\n const hasChunks = Object.keys(chunks).length > 0;\n\n if (!hasChunks) {\n const raw = this.getTranslations(locale);\n\n return `window.__i18n__=${JSON.stringify({ locale, hashes: {}, imports: {}, translations: raw })};`;\n }\n\n const hashes = this.getHashes(locale);\n\n // collect all unique chunk names used in hashes and imports\n const allChunks = new Set<string>(Object.keys(hashes));\n\n for (const deps of Object.values(imports)) {\n deps.forEach((d) => allChunks.add(d));\n }\n\n // generate short variable names: a, b, c, ..., z, aa, ab, ...\n const chunkToVar = new Map<string, string>();\n let varIndex = 0;\n\n for (const chunk of allChunks) {\n chunkToVar.set(chunk, generateBB26(varIndex++));\n }\n\n // build IIFE with aliases for compact output\n const varDecls = [...chunkToVar.entries()].map(([chunk, v]) => `${v}=${JSON.stringify(chunk)}`).join(',');\n\n const hashesObj = Object.entries(hashes)\n .map(([chunk, hash]) => `[${chunkToVar.get(chunk)}]:${JSON.stringify(hash)}`)\n .join(',');\n\n // only include imports that are in our chunk set (have translations for this locale)\n const importsObj = Object.entries(imports)\n .filter(([chunk]) => chunkToVar.has(chunk))\n .map(([chunk, deps]) => {\n const validDeps = deps.filter((d) => chunkToVar.has(d));\n\n return validDeps.length > 0\n ? `[${chunkToVar.get(chunk)}]:[${validDeps.map((d) => chunkToVar.get(d)).join(',')}]`\n : null;\n })\n .filter(Boolean)\n .join(',');\n\n return (\n `(()=>{var ${varDecls};` +\n `window.__i18n__={locale:${JSON.stringify(locale)},hashes:{${hashesObj}},imports:{${importsObj}},translations:{}};` +\n `})();`\n );\n }\n\n /**\n * Clear translations for specified locale(s).\n * @param locales - Locale(s) to clear, or all if not specified\n */\n clear(locales?: string | string[] | undefined): void {\n const targets = this.resolveLocales(locales);\n\n for (const locale of targets) {\n this.rawCache.delete(locale);\n this.mergedCache.delete(locale);\n this.compiledCache.delete(locale);\n this.hashCache.delete(locale);\n this.scriptCache.delete(locale);\n\n // clear chunk cache entries for this locale\n for (const key of this.chunkCache.keys()) {\n if (key.startsWith(`${locale}:`)) {\n this.chunkCache.delete(key);\n }\n }\n }\n }\n\n /**\n * invalidate derived caches when the extraction manifest has changed (dev mode HMR)\n */\n private invalidateIfManifestChanged(): void {\n const { version } = getGlobalState();\n\n if (version === this.manifestVersion) {\n return;\n }\n\n this.manifestVersion = version;\n this.mergedCache.clear();\n this.compiledCache.clear();\n this.scriptCache.clear();\n this.chunkCache.clear();\n }\n\n private resolveLocales(locales?: string | string[] | undefined): string[] {\n if (!locales) {\n return this.config?.locales ?? [];\n }\n\n if (typeof locales === 'string') {\n return [locales];\n }\n\n return locales;\n }\n}\n\nexport const i18n = new I18nSingleton();\n","import { AsyncLocalStorage } from 'node:async_hooks';\nimport type { I18nContext } from './types.js';\n\nconst als = new AsyncLocalStorage<I18nContext>();\n\n/**\n * Run a function with i18n context (called by middleware)\n */\nexport function runWithContext<T>(context: I18nContext, fn: () => T): T {\n return als.run(context, fn);\n}\n\n/**\n * Get the current i18n context\n */\nexport function getContext(): I18nContext | null {\n return als.getStore() ?? null;\n}\n"],"mappings":"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"}
|