@astroscope/i18n 0.2.0 → 0.2.2

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.
Files changed (33) hide show
  1. package/README.md +2 -0
  2. package/dist/{babel-plugin-K2K4ZPM5.js → babel-plugin-GOB6FMZ2.js} +3 -2
  3. package/dist/{chunk-FAKBSLNW.js → chunk-7MSGHEST.js} +14 -7
  4. package/dist/chunk-FW6TPWMT.js +358 -0
  5. package/dist/{chunk-QFAAGCP5.js → chunk-ZKXMIGSF.js} +97 -1
  6. package/dist/client/directives/idle.d.ts +4 -1
  7. package/dist/client/directives/idle.js +13 -6
  8. package/dist/client/directives/load.d.ts +4 -1
  9. package/dist/client/directives/load.js +6 -6
  10. package/dist/client/directives/media.d.ts +4 -1
  11. package/dist/client/directives/media.js +16 -6
  12. package/dist/client/directives/only.d.ts +6 -1
  13. package/dist/client/directives/only.js +6 -6
  14. package/dist/client/directives/utils.d.ts +6 -5
  15. package/dist/client/directives/utils.js +3 -3
  16. package/dist/client/directives/visible.d.ts +4 -1
  17. package/dist/client/directives/visible.js +21 -6
  18. package/dist/client/t.d.ts +2 -2
  19. package/dist/extraction/index.d.ts +3 -3
  20. package/dist/extraction/index.js +1 -1
  21. package/dist/{i18n-BGfrSNM8.d.ts → i18n-BBAAVYz_.d.ts} +16 -2
  22. package/dist/index.d.ts +3 -3
  23. package/dist/index.js +14 -20
  24. package/dist/internal.d.ts +4 -4
  25. package/dist/internal.js +1 -1
  26. package/dist/server/t.d.ts +1 -1
  27. package/dist/server/t.js +1 -1
  28. package/dist/{types-B_-RxW4A.d.ts → types-BeDFfFIL.d.ts} +1 -1
  29. package/dist/{types-Bzp6KxsI.d.ts → types-DNRFj1XA.d.ts} +1 -0
  30. package/dist/{types-8uKCwFda.d.ts → types-DTUho6gi.d.ts} +7 -1
  31. package/package.json +4 -2
  32. package/src/components/I18nScript.astro +2 -23
  33. package/dist/chunk-JJEDOJBR.js +0 -193
package/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # @astroscope/i18n
2
2
 
3
+ > **Note:** This package is in active development. APIs may change between versions.
4
+
3
5
  i18n for Astro + React islands — automatic tree-shaking, parallel loading, any translation source.
4
6
 
5
7
  ## Why this library?
@@ -103,10 +103,11 @@ function i18nExtractPlugin({ types: t }) {
103
103
  const meta = args.length >= 2 && t.isExpression(args[1]) ? extractMeta(t, args[1], warn) : { fallback: "" };
104
104
  state.opts.onKeyExtracted({ key, meta, file, line });
105
105
  if (state.opts.stripFallbacks && args.length >= 2) {
106
+ const firstArg = args[0];
106
107
  if (args.length === 2) {
107
- path.node.arguments = [args[0]];
108
+ path.node.arguments = [firstArg];
108
109
  } else {
109
- path.node.arguments = [args[0], t.identifier("undefined"), args[2]];
110
+ path.node.arguments = [firstArg, t.identifier("undefined"), args[2]];
110
111
  }
111
112
  }
112
113
  }
@@ -4,21 +4,28 @@ import {
4
4
  } from "./chunk-4A5BKHFN.js";
5
5
 
6
6
  // src/client/directives/utils.ts
7
- async function loadI18nForChunk(el) {
8
- const componentUrl = el.getAttribute("component-url");
9
- if (!componentUrl) return;
10
- const chunkName = componentUrlToChunkName(componentUrl);
11
- const i18n = window.__i18n__;
7
+ function prefetchTranslation(chunkName, i18n) {
12
8
  const hash = i18n.hashes[chunkName];
13
9
  if (hash) {
14
10
  const url = buildI18nChunkUrl(i18n.locale, chunkName, hash);
15
- await import(
11
+ import(
16
12
  /* @vite-ignore */
17
13
  url
18
14
  );
19
15
  }
20
16
  }
17
+ function warmUpI18nForChunk(el) {
18
+ const componentUrl = el.getAttribute("component-url");
19
+ if (!componentUrl) return;
20
+ const chunkName = componentUrlToChunkName(componentUrl);
21
+ const i18n = window.__i18n__;
22
+ prefetchTranslation(chunkName, i18n);
23
+ const deps = i18n.imports[chunkName] ?? [];
24
+ for (const dep of deps) {
25
+ prefetchTranslation(dep, i18n);
26
+ }
27
+ }
21
28
 
22
29
  export {
23
- loadI18nForChunk
30
+ warmUpI18nForChunk
24
31
  };
@@ -0,0 +1,358 @@
1
+ import {
2
+ chunkIdToName
3
+ } from "./chunk-4A5BKHFN.js";
4
+
5
+ // src/extraction/manifest.ts
6
+ import path from "path";
7
+ var I18N_MANIFEST_GLOBAL_KEY = "__astroscope_i18n_manifest__";
8
+ function getGlobalState() {
9
+ const g = globalThis;
10
+ if (!g[I18N_MANIFEST_GLOBAL_KEY]) {
11
+ g[I18N_MANIFEST_GLOBAL_KEY] = {
12
+ extractedKeys: [],
13
+ chunkManifest: {},
14
+ importsManifest: {},
15
+ projectRoot: ""
16
+ };
17
+ }
18
+ return g[I18N_MANIFEST_GLOBAL_KEY];
19
+ }
20
+ function getManifest() {
21
+ const state = getGlobalState();
22
+ const keys = state.extractedKeys.map((key) => ({
23
+ ...key,
24
+ // make file paths relative to project root for better readability
25
+ // and avoiding leaking absolute paths from dev or ci environments
26
+ file: state.projectRoot ? path.relative(state.projectRoot, key.file) : key.file
27
+ }));
28
+ return { keys, chunks: state.chunkManifest, imports: state.importsManifest };
29
+ }
30
+
31
+ // src/extraction/vite-plugin.ts
32
+ import fs2 from "fs";
33
+ import path2 from "path";
34
+ import MagicString from "magic-string";
35
+
36
+ // src/extraction/extract.ts
37
+ var TS_EXTENSIONS = [".ts", ".tsx"];
38
+ var JSX_EXTENSIONS = [".jsx", ".tsx"];
39
+ var JS_EXTENSIONS = [".js", ".jsx"];
40
+ var BABEL_EXTENSIONS = [...TS_EXTENSIONS, ...JS_EXTENSIONS];
41
+ var ALL_EXTENSIONS = [...BABEL_EXTENSIONS, ".astro"];
42
+ async function extractKeysFromFile(options) {
43
+ const { filename, code, logger, stripFallbacks = false, onKeyExtracted } = options;
44
+ const keys = [];
45
+ const [{ transformAsync }, { i18nExtractPlugin }] = await Promise.all([
46
+ import("@babel/core"),
47
+ import("./babel-plugin-GOB6FMZ2.js")
48
+ ]);
49
+ const result = await transformAsync(code, {
50
+ filename,
51
+ sourceMaps: stripFallbacks,
52
+ // only need source maps when transforming
53
+ babelrc: false,
54
+ configFile: false,
55
+ parserOpts: {
56
+ plugins: [
57
+ ...TS_EXTENSIONS.some((ext) => filename.endsWith(ext)) ? ["typescript"] : [],
58
+ ...JSX_EXTENSIONS.some((ext) => filename.endsWith(ext)) ? ["jsx"] : []
59
+ ]
60
+ },
61
+ plugins: [
62
+ [
63
+ i18nExtractPlugin,
64
+ {
65
+ stripFallbacks,
66
+ onKeyExtracted: (key) => {
67
+ keys.push(key);
68
+ onKeyExtracted?.(key);
69
+ },
70
+ logger
71
+ }
72
+ ]
73
+ ]
74
+ });
75
+ return {
76
+ keys,
77
+ code: result?.code ?? null,
78
+ map: result?.map
79
+ };
80
+ }
81
+
82
+ // src/extraction/key-store.ts
83
+ var KeyStore = class {
84
+ constructor(logger) {
85
+ this.logger = logger;
86
+ }
87
+ extractedKeys = [];
88
+ fileToKeys = /* @__PURE__ */ new Map();
89
+ filesWithI18n = /* @__PURE__ */ new Set();
90
+ /**
91
+ * Add keys for a file, replacing any existing keys for that file.
92
+ */
93
+ addFileKeys(filename, keys) {
94
+ const oldKeys = this.fileToKeys.get(filename);
95
+ const newKeyStrings = keys.map((k) => k.key);
96
+ if (oldKeys) {
97
+ const added = newKeyStrings.filter((k) => !oldKeys.includes(k));
98
+ const removed = oldKeys.filter((k) => !newKeyStrings.includes(k));
99
+ const file = filename.split("/").pop();
100
+ if (added.length > 0 || removed.length > 0) {
101
+ const parts = [];
102
+ if (added.length > 0) parts.push(`+${added.length}`);
103
+ if (removed.length > 0) parts.push(`-${removed.length}`);
104
+ this.logger.info(`hmr: ${parts.join(" ")} key(s) in ${file}`);
105
+ }
106
+ }
107
+ if (oldKeys) {
108
+ for (let i = this.extractedKeys.length - 1; i >= 0; i--) {
109
+ if (this.extractedKeys[i]?.file === filename) {
110
+ this.extractedKeys.splice(i, 1);
111
+ }
112
+ }
113
+ }
114
+ this.filesWithI18n.add(filename);
115
+ if (keys.length > 0) {
116
+ this.extractedKeys.push(...keys);
117
+ }
118
+ this.fileToKeys.set(filename, newKeyStrings);
119
+ }
120
+ /**
121
+ * Get count of unique keys.
122
+ */
123
+ get uniqueKeyCount() {
124
+ return new Set(this.extractedKeys.map((k) => k.key)).size;
125
+ }
126
+ /**
127
+ * Merge another store's data into this one.
128
+ */
129
+ merge(other) {
130
+ for (const [file, keys] of other.fileToKeys) {
131
+ this.fileToKeys.set(file, keys);
132
+ }
133
+ for (const file of other.filesWithI18n) {
134
+ this.filesWithI18n.add(file);
135
+ }
136
+ this.extractedKeys.push(...other.extractedKeys);
137
+ }
138
+ };
139
+
140
+ // src/extraction/scan.ts
141
+ import fs from "fs";
142
+ import { glob } from "glob";
143
+ var GLOB_PATTERN = `src/**/*.{${ALL_EXTENSIONS.map((e) => e.slice(1)).join(",")}}`;
144
+ async function compileAstro(code, filename) {
145
+ try {
146
+ const { transform } = await import("@astrojs/compiler");
147
+ const result = await transform(code, { filename });
148
+ return result.code;
149
+ } catch {
150
+ return null;
151
+ }
152
+ }
153
+ async function scan(projectRoot, logger) {
154
+ const store = new KeyStore(logger);
155
+ const files = await glob(GLOB_PATTERN, {
156
+ cwd: projectRoot,
157
+ absolute: true,
158
+ ignore: ["**/node_modules/**"]
159
+ });
160
+ const results = await Promise.all(
161
+ files.map(async (file) => {
162
+ let code = await fs.promises.readFile(file, "utf-8");
163
+ if (!code.includes("@astroscope/i18n/t")) {
164
+ return { file, keys: null };
165
+ }
166
+ if (file.endsWith(".astro")) {
167
+ const compiled = await compileAstro(code, file);
168
+ if (!compiled) {
169
+ return { file, keys: null };
170
+ }
171
+ code = compiled;
172
+ }
173
+ const result = await extractKeysFromFile({
174
+ filename: file,
175
+ code,
176
+ logger,
177
+ stripFallbacks: false
178
+ });
179
+ return { file, keys: result.keys.length > 0 ? result.keys : null };
180
+ })
181
+ );
182
+ for (const { file, keys } of results) {
183
+ if (keys) {
184
+ store.addFileKeys(file, keys);
185
+ }
186
+ }
187
+ return store;
188
+ }
189
+
190
+ // src/extraction/vite-plugin.ts
191
+ var VIRTUAL_MODULE_ID = "virtual:@astroscope/i18n/manifest";
192
+ var isVirtualModuleId = (id) => id === VIRTUAL_MODULE_ID || id.startsWith(`${VIRTUAL_MODULE_ID}?`);
193
+ var RESOLVED_VIRTUAL_MODULE_ID = `\0${VIRTUAL_MODULE_ID}`;
194
+ var isResolvedVirtualModuleId = (id) => id === RESOLVED_VIRTUAL_MODULE_ID || id.startsWith(`\0${VIRTUAL_MODULE_ID}?`);
195
+ var MANIFEST_FILE_NAME = "i18n-manifest.json";
196
+ function i18nVitePlugin(options) {
197
+ const { logger } = options;
198
+ const store = new KeyStore(logger);
199
+ let isBuild = false;
200
+ let isSSR = false;
201
+ let projectRoot = "";
202
+ return {
203
+ name: "@astroscope/i18n/extract",
204
+ enforce: "pre",
205
+ configResolved(config) {
206
+ const state = getGlobalState();
207
+ state.projectRoot = config.root;
208
+ state.extractedKeys = store.extractedKeys;
209
+ isBuild = config.command === "build";
210
+ isSSR = !!config.build.ssr;
211
+ projectRoot = config.root;
212
+ },
213
+ async configureServer(server) {
214
+ if (server.config.isProduction || isBuild) {
215
+ return;
216
+ }
217
+ const result = await scan(projectRoot, logger);
218
+ store.merge(result);
219
+ logger.info(`dev mode: scanned ${result.fileToKeys.size} files, found ${result.uniqueKeyCount} keys`);
220
+ },
221
+ resolveId(id) {
222
+ if (isVirtualModuleId(id)) {
223
+ return `\0${id}`;
224
+ }
225
+ },
226
+ load(id) {
227
+ if (isResolvedVirtualModuleId(id)) {
228
+ if (isBuild) {
229
+ return `
230
+ import { readFileSync } from 'node:fs';
231
+ import { fileURLToPath } from 'node:url';
232
+ import { dirname, join } from 'node:path';
233
+
234
+ const __dirname = dirname(fileURLToPath(import.meta.url));
235
+ const manifestJson = JSON.parse(readFileSync(join(__dirname, '${MANIFEST_FILE_NAME}'), 'utf-8'));
236
+
237
+ export const manifest = manifestJson;
238
+ export function getManifest() { return manifestJson; }
239
+ `;
240
+ }
241
+ return `
242
+ import { getManifest as _getManifest } from '@astroscope/i18n/extraction';
243
+ export const manifest = { keys: [], chunks: {} };
244
+ export function getManifest() { return _getManifest(); }
245
+ `;
246
+ }
247
+ },
248
+ async transform(code, filename) {
249
+ if (!ALL_EXTENSIONS.some((ext) => filename.endsWith(ext))) {
250
+ return null;
251
+ }
252
+ if (filename.includes("node_modules")) {
253
+ return null;
254
+ }
255
+ if (!code.includes("@astroscope/i18n/t")) {
256
+ return null;
257
+ }
258
+ const result = await extractKeysFromFile({
259
+ filename,
260
+ code,
261
+ logger,
262
+ stripFallbacks: isBuild
263
+ });
264
+ store.addFileKeys(filename, result.keys);
265
+ if (isBuild && result.code) {
266
+ return { code: result.code, map: result.map };
267
+ }
268
+ return null;
269
+ },
270
+ generateBundle(_, bundle) {
271
+ const state = getGlobalState();
272
+ state.chunkManifest = {};
273
+ state.importsManifest = {};
274
+ const chunksWithI18n = /* @__PURE__ */ new Set();
275
+ const chunkNameToFileName = /* @__PURE__ */ new Map();
276
+ const directImports = /* @__PURE__ */ new Map();
277
+ for (const [fileName, chunk] of Object.entries(bundle)) {
278
+ if (chunk.type === "chunk" && chunk.moduleIds) {
279
+ const keys = /* @__PURE__ */ new Set();
280
+ let hasI18nModule = false;
281
+ for (const moduleId of chunk.moduleIds) {
282
+ const moduleKeys = store.fileToKeys.get(moduleId);
283
+ moduleKeys?.forEach((k) => keys.add(k));
284
+ if (store.filesWithI18n.has(moduleId)) {
285
+ hasI18nModule = true;
286
+ }
287
+ }
288
+ const chunkName = chunkIdToName(fileName.replace(/\.js$/, ""));
289
+ if (keys.size) {
290
+ state.chunkManifest[chunkName] = Array.from(keys);
291
+ }
292
+ if (hasI18nModule) {
293
+ chunksWithI18n.add(chunkName);
294
+ }
295
+ chunkNameToFileName.set(chunkName, fileName);
296
+ const imports = [];
297
+ for (const importedFile of chunk.imports) {
298
+ const baseName = importedFile.replace(/^.*\//, "").replace(/\.js$/, "");
299
+ const importedChunkName = chunkIdToName(baseName);
300
+ imports.push(importedChunkName);
301
+ }
302
+ directImports.set(chunkName, imports);
303
+ }
304
+ }
305
+ const flattenImports = (chunkName, visited) => {
306
+ if (visited.has(chunkName)) return [];
307
+ visited.add(chunkName);
308
+ const result = /* @__PURE__ */ new Set();
309
+ const imports = directImports.get(chunkName) ?? [];
310
+ for (const imported of imports) {
311
+ if (state.chunkManifest[imported]) {
312
+ result.add(imported);
313
+ }
314
+ for (const descendant of flattenImports(imported, visited)) {
315
+ result.add(descendant);
316
+ }
317
+ }
318
+ visited.delete(chunkName);
319
+ return Array.from(result);
320
+ };
321
+ for (const chunkName of directImports.keys()) {
322
+ const flattened = flattenImports(chunkName, /* @__PURE__ */ new Set());
323
+ if (flattened.length > 0) {
324
+ state.importsManifest[chunkName] = flattened;
325
+ }
326
+ }
327
+ for (const [fileName, chunk] of Object.entries(bundle)) {
328
+ if (chunk.type !== "chunk") continue;
329
+ const chunkName = chunkIdToName(fileName.replace(/\.js$/, ""));
330
+ if (!chunksWithI18n.has(chunkName)) continue;
331
+ const loaderCode = `if(typeof window!=='undefined'&&window.__i18n__){const _=window.__i18n__,h=_.hashes[${JSON.stringify(chunkName)}];if(h)await import(\`/_i18n/\${_.locale}/${chunkName}.\${h}.js\`);}`;
332
+ const s = new MagicString(chunk.code);
333
+ s.prepend(loaderCode);
334
+ chunk.code = s.toString();
335
+ if (chunk.map) {
336
+ chunk.map = s.generateMap({ hires: true });
337
+ }
338
+ }
339
+ logger.info(`manifest: ${Object.keys(state.chunkManifest).length} chunks, ${store.uniqueKeyCount} keys`);
340
+ },
341
+ writeBundle(outputOptions) {
342
+ if (!isSSR && outputOptions.dir) {
343
+ const chunksDir = path2.resolve(outputOptions.dir, "..", "server", "chunks");
344
+ const manifestPath = path2.join(chunksDir, MANIFEST_FILE_NAME);
345
+ if (fs2.existsSync(chunksDir)) {
346
+ fs2.writeFileSync(manifestPath, JSON.stringify(getManifest()));
347
+ } else {
348
+ logger.error(`server chunks directory not found, cannot write manifest to ${manifestPath}`);
349
+ }
350
+ }
351
+ }
352
+ };
353
+ }
354
+
355
+ export {
356
+ getManifest,
357
+ i18nVitePlugin
358
+ };
@@ -24,6 +24,21 @@ function computeAllChunkHashes(translations, manifest) {
24
24
  return hashes;
25
25
  }
26
26
 
27
+ // src/server/utils.ts
28
+ var LOWERCASE_A = 97;
29
+ function indexToLetter(index) {
30
+ return String.fromCharCode(LOWERCASE_A + index);
31
+ }
32
+ function generateBB26(index) {
33
+ let name = "";
34
+ let remaining = index;
35
+ do {
36
+ name = indexToLetter(remaining % 26) + name;
37
+ remaining = Math.floor(remaining / 26) - 1;
38
+ } while (remaining >= 0);
39
+ return name;
40
+ }
41
+
27
42
  // src/server/i18n.ts
28
43
  var I18nSingleton = class {
29
44
  // normalized user config with defaults applied
@@ -36,6 +51,10 @@ var I18nSingleton = class {
36
51
  compiledCache = /* @__PURE__ */ new Map();
37
52
  // locale -> chunk name -> content hash (for cache busting)
38
53
  hashCache = /* @__PURE__ */ new Map();
54
+ // locale -> inline script for I18nScript component
55
+ scriptCache = /* @__PURE__ */ new Map();
56
+ // "locale:chunkName" -> encoded chunk response body
57
+ chunkCache = /* @__PURE__ */ new Map();
39
58
  // manifest getter (set by init module, provides live data in dev mode)
40
59
  manifestGetter = null;
41
60
  async configure(config) {
@@ -88,6 +107,7 @@ var I18nSingleton = class {
88
107
  this.mergedCache.delete(locale);
89
108
  this.compiledCache.delete(locale);
90
109
  this.hashCache.delete(locale);
110
+ this.scriptCache.delete(locale);
91
111
  const { chunks } = this.getManifest();
92
112
  if (Object.keys(chunks).length > 0) {
93
113
  this.hashCache.set(locale, computeAllChunkHashes(raw, chunks));
@@ -99,7 +119,7 @@ var I18nSingleton = class {
99
119
  return cached;
100
120
  }
101
121
  const raw = this.rawCache.get(locale) ?? {};
102
- const manifest = this.manifestGetter?.() ?? { keys: [], chunks: {} };
122
+ const manifest = this.manifestGetter?.() ?? { keys: [], chunks: {}, imports: {} };
103
123
  if (manifest.keys.length === 0) {
104
124
  return raw;
105
125
  }
@@ -135,6 +155,76 @@ var I18nSingleton = class {
135
155
  getHashes(locale) {
136
156
  return this.hashCache.get(locale) ?? {};
137
157
  }
158
+ /**
159
+ * Get the encoded chunk response body for a locale/chunk combination.
160
+ * Cached to avoid repeated JSON.stringify and encoding on each request.
161
+ * Returns undefined if chunk not found in manifest.
162
+ */
163
+ getChunkBody(locale, chunkName) {
164
+ const keys = this.getManifest().chunks[chunkName];
165
+ if (!keys) {
166
+ return void 0;
167
+ }
168
+ const cacheKey = `${locale}:${chunkName}`;
169
+ const cached = this.chunkCache.get(cacheKey);
170
+ if (cached) {
171
+ return cached;
172
+ }
173
+ const translations = this.getTranslations(locale);
174
+ const chunkTranslations = {};
175
+ for (const key of keys) {
176
+ if (translations[key]) {
177
+ chunkTranslations[key] = translations[key];
178
+ }
179
+ }
180
+ const js = `/* @astroscope/i18n chunk: ${chunkName} */
181
+ (function() {
182
+ var i = window.__i18n__;
183
+ if (i) Object.assign(i.translations, ${JSON.stringify(chunkTranslations)});
184
+ })();
185
+ `;
186
+ const body = new TextEncoder().encode(js);
187
+ this.chunkCache.set(cacheKey, body);
188
+ return body;
189
+ }
190
+ /**
191
+ * Get the inline script for I18nScript component.
192
+ * Cached per locale, invalidated when translations change.
193
+ */
194
+ getClientScript(locale) {
195
+ const cached = this.scriptCache.get(locale);
196
+ if (cached) {
197
+ return cached;
198
+ }
199
+ const script = this.createClientScript(locale);
200
+ this.scriptCache.set(locale, script);
201
+ return script;
202
+ }
203
+ createClientScript(locale) {
204
+ const { chunks, imports } = this.getManifest();
205
+ const hasChunks = Object.keys(chunks).length > 0;
206
+ if (!hasChunks) {
207
+ const raw = this.getTranslations(locale);
208
+ return `window.__i18n__=${JSON.stringify({ locale, hashes: {}, imports: {}, translations: raw })};`;
209
+ }
210
+ const hashes = this.getHashes(locale);
211
+ const allChunks = new Set(Object.keys(hashes));
212
+ for (const deps of Object.values(imports)) {
213
+ deps.forEach((d) => allChunks.add(d));
214
+ }
215
+ const chunkToVar = /* @__PURE__ */ new Map();
216
+ let varIndex = 0;
217
+ for (const chunk of allChunks) {
218
+ chunkToVar.set(chunk, generateBB26(varIndex++));
219
+ }
220
+ const varDecls = [...chunkToVar.entries()].map(([chunk, v]) => `${v}=${JSON.stringify(chunk)}`).join(",");
221
+ const hashesObj = Object.entries(hashes).map(([chunk, hash]) => `[${chunkToVar.get(chunk)}]:${JSON.stringify(hash)}`).join(",");
222
+ const importsObj = Object.entries(imports).filter(([chunk]) => chunkToVar.has(chunk)).map(([chunk, deps]) => {
223
+ const validDeps = deps.filter((d) => chunkToVar.has(d));
224
+ return validDeps.length > 0 ? `[${chunkToVar.get(chunk)}]:[${validDeps.map((d) => chunkToVar.get(d)).join(",")}]` : null;
225
+ }).filter(Boolean).join(",");
226
+ return `(()=>{var ${varDecls};window.__i18n__={locale:${JSON.stringify(locale)},hashes:{${hashesObj}},imports:{${importsObj}},translations:{}};})();`;
227
+ }
138
228
  /**
139
229
  * Clear translations for specified locale(s).
140
230
  * @param locales - Locale(s) to clear, or all if not specified
@@ -146,6 +236,12 @@ var I18nSingleton = class {
146
236
  this.mergedCache.delete(locale);
147
237
  this.compiledCache.delete(locale);
148
238
  this.hashCache.delete(locale);
239
+ this.scriptCache.delete(locale);
240
+ for (const key of this.chunkCache.keys()) {
241
+ if (key.startsWith(`${locale}:`)) {
242
+ this.chunkCache.delete(key);
243
+ }
244
+ }
149
245
  }
150
246
  }
151
247
  resolveLocales(locales) {
@@ -2,8 +2,11 @@ import { ClientDirective } from 'astro';
2
2
 
3
3
  /**
4
4
  * i18n-aware client:idle directive
5
- * Loads translations in parallel before delegating to Astro's original idle directive
5
+ *
6
+ * Based on Astro's idle directive (MIT License)
7
+ * https://github.com/withastro/astro/blob/main/packages/astro/src/runtime/client/idle.ts
6
8
  */
9
+
7
10
  declare const idleDirective: ClientDirective;
8
11
 
9
12
  export { idleDirective as default };
@@ -1,13 +1,20 @@
1
1
  import {
2
- loadI18nForChunk
3
- } from "../../chunk-FAKBSLNW.js";
2
+ warmUpI18nForChunk
3
+ } from "../../chunk-7MSGHEST.js";
4
4
  import "../../chunk-4A5BKHFN.js";
5
5
 
6
6
  // src/client/directives/idle.ts
7
- import originalIdle from "astro/client/idle.js";
8
- var idleDirective = async (load, options, el) => {
9
- await loadI18nForChunk(el);
10
- return originalIdle(load, options, el);
7
+ var idleDirective = (load, _options, el) => {
8
+ const cb = async () => {
9
+ warmUpI18nForChunk(el);
10
+ const hydrate = await load();
11
+ await hydrate();
12
+ };
13
+ if ("requestIdleCallback" in window) {
14
+ window.requestIdleCallback(cb);
15
+ } else {
16
+ setTimeout(cb, 200);
17
+ }
11
18
  };
12
19
  var idle_default = idleDirective;
13
20
  export {
@@ -2,8 +2,11 @@ import { ClientDirective } from 'astro';
2
2
 
3
3
  /**
4
4
  * i18n-aware client:load directive
5
- * Loads translations in parallel before delegating to Astro's original load directive
5
+ *
6
+ * Based on Astro's load directive (MIT License)
7
+ * https://github.com/withastro/astro/blob/main/packages/astro/src/runtime/client/load.ts
6
8
  */
9
+
7
10
  declare const loadDirective: ClientDirective;
8
11
 
9
12
  export { loadDirective as default };
@@ -1,13 +1,13 @@
1
1
  import {
2
- loadI18nForChunk
3
- } from "../../chunk-FAKBSLNW.js";
2
+ warmUpI18nForChunk
3
+ } from "../../chunk-7MSGHEST.js";
4
4
  import "../../chunk-4A5BKHFN.js";
5
5
 
6
6
  // src/client/directives/load.ts
7
- import originalLoad from "astro/client/load.js";
8
- var loadDirective = async (load, options, el) => {
9
- await loadI18nForChunk(el);
10
- return originalLoad(load, options, el);
7
+ var loadDirective = async (load, _options, el) => {
8
+ warmUpI18nForChunk(el);
9
+ const hydrate = await load();
10
+ await hydrate();
11
11
  };
12
12
  var load_default = loadDirective;
13
13
  export {
@@ -2,8 +2,11 @@ import { ClientDirective } from 'astro';
2
2
 
3
3
  /**
4
4
  * i18n-aware client:media directive
5
- * Loads translations in parallel before delegating to Astro's original media directive
5
+ *
6
+ * Based on Astro's media directive (MIT License)
7
+ * https://github.com/withastro/astro/blob/main/packages/astro/src/runtime/client/media.ts
6
8
  */
9
+
7
10
  declare const mediaDirective: ClientDirective;
8
11
 
9
12
  export { mediaDirective as default };
@@ -1,13 +1,23 @@
1
1
  import {
2
- loadI18nForChunk
3
- } from "../../chunk-FAKBSLNW.js";
2
+ warmUpI18nForChunk
3
+ } from "../../chunk-7MSGHEST.js";
4
4
  import "../../chunk-4A5BKHFN.js";
5
5
 
6
6
  // src/client/directives/media.ts
7
- import originalMedia from "astro/client/media.js";
8
- var mediaDirective = async (load, options, el) => {
9
- await loadI18nForChunk(el);
10
- return originalMedia(load, options, el);
7
+ var mediaDirective = (load, options, el) => {
8
+ const cb = async () => {
9
+ warmUpI18nForChunk(el);
10
+ const hydrate = await load();
11
+ await hydrate();
12
+ };
13
+ if (options.value) {
14
+ const mql = matchMedia(options.value);
15
+ if (mql.matches) {
16
+ cb();
17
+ } else {
18
+ mql.addEventListener("change", cb, { once: true });
19
+ }
20
+ }
11
21
  };
12
22
  var media_default = mediaDirective;
13
23
  export {
@@ -2,8 +2,13 @@ import { ClientDirective } from 'astro';
2
2
 
3
3
  /**
4
4
  * i18n-aware client:only directive
5
- * Loads translations in parallel before delegating to Astro's original only directive
5
+ *
6
+ * Based on Astro's load directive (MIT License)
7
+ * https://github.com/withastro/astro/blob/main/packages/astro/src/runtime/client/load.ts
8
+ *
9
+ * client:only works the same as client:load - immediate hydration
6
10
  */
11
+
7
12
  declare const onlyDirective: ClientDirective;
8
13
 
9
14
  export { onlyDirective as default };
@@ -1,13 +1,13 @@
1
1
  import {
2
- loadI18nForChunk
3
- } from "../../chunk-FAKBSLNW.js";
2
+ warmUpI18nForChunk
3
+ } from "../../chunk-7MSGHEST.js";
4
4
  import "../../chunk-4A5BKHFN.js";
5
5
 
6
6
  // src/client/directives/only.ts
7
- import originalOnly from "astro/client/only.js";
8
- var onlyDirective = async (load, options, el) => {
9
- await loadI18nForChunk(el);
10
- return originalOnly(load, options, el);
7
+ var onlyDirective = async (load, _options, el) => {
8
+ warmUpI18nForChunk(el);
9
+ const hydrate = await load();
10
+ await hydrate();
11
11
  };
12
12
  var only_default = onlyDirective;
13
13
  export {
@@ -1,10 +1,11 @@
1
- import '../../types-B_-RxW4A.js';
2
- import '../../types-Bzp6KxsI.js';
1
+ import '../../types-BeDFfFIL.js';
2
+ import '../../types-DNRFj1XA.js';
3
3
 
4
4
  /**
5
- * Load translations for a chunk before hydration
5
+ * Warm up translation cache for a chunk and all its descendants (fire and forget)
6
6
  * Extracts chunk name from astro-island's component-url attribute
7
+ * Uses flattened imports map from window.__i18n__ to prefetch all nested translations
7
8
  */
8
- declare function loadI18nForChunk(el: HTMLElement): Promise<void>;
9
+ declare function warmUpI18nForChunk(el: HTMLElement): void;
9
10
 
10
- export { loadI18nForChunk };
11
+ export { warmUpI18nForChunk };
@@ -1,7 +1,7 @@
1
1
  import {
2
- loadI18nForChunk
3
- } from "../../chunk-FAKBSLNW.js";
2
+ warmUpI18nForChunk
3
+ } from "../../chunk-7MSGHEST.js";
4
4
  import "../../chunk-4A5BKHFN.js";
5
5
  export {
6
- loadI18nForChunk
6
+ warmUpI18nForChunk
7
7
  };
@@ -2,8 +2,11 @@ import { ClientDirective } from 'astro';
2
2
 
3
3
  /**
4
4
  * i18n-aware client:visible directive
5
- * Loads translations in parallel before delegating to Astro's original visible directive
5
+ *
6
+ * Based on Astro's visible directive (MIT License)
7
+ * https://github.com/withastro/astro/blob/main/packages/astro/src/runtime/client/visible.ts
6
8
  */
9
+
7
10
  declare const visibleDirective: ClientDirective;
8
11
 
9
12
  export { visibleDirective as default };
@@ -1,13 +1,28 @@
1
1
  import {
2
- loadI18nForChunk
3
- } from "../../chunk-FAKBSLNW.js";
2
+ warmUpI18nForChunk
3
+ } from "../../chunk-7MSGHEST.js";
4
4
  import "../../chunk-4A5BKHFN.js";
5
5
 
6
6
  // src/client/directives/visible.ts
7
- import originalVisible from "astro/client/visible.js";
8
- var visibleDirective = async (load, options, el) => {
9
- await loadI18nForChunk(el);
10
- return originalVisible(load, options, el);
7
+ var visibleDirective = (load, options, el) => {
8
+ const cb = async () => {
9
+ warmUpI18nForChunk(el);
10
+ const hydrate = await load();
11
+ await hydrate();
12
+ };
13
+ const rawOptions = typeof options.value === "object" ? options.value : void 0;
14
+ const ioOptions = rawOptions?.rootMargin ? { rootMargin: rawOptions.rootMargin } : {};
15
+ const io = new IntersectionObserver((entries) => {
16
+ for (const entry of entries) {
17
+ if (!entry.isIntersecting) continue;
18
+ io.disconnect();
19
+ cb();
20
+ break;
21
+ }
22
+ }, ioOptions);
23
+ for (const child of el.children) {
24
+ io.observe(child);
25
+ }
11
26
  };
12
27
  var visible_default = visibleDirective;
13
28
  export {
@@ -1,5 +1,5 @@
1
- import { a as TranslateFunction } from '../types-Bzp6KxsI.js';
2
- import '../types-B_-RxW4A.js';
1
+ import { a as TranslateFunction } from '../types-DNRFj1XA.js';
2
+ import '../types-BeDFfFIL.js';
3
3
 
4
4
  declare function getLocale(): string;
5
5
  /**
@@ -1,8 +1,8 @@
1
- import { a as ExtractionManifest } from '../types-8uKCwFda.js';
2
- export { C as ChunkManifest, E as ExtractedKey } from '../types-8uKCwFda.js';
1
+ import { a as ExtractionManifest } from '../types-DTUho6gi.js';
2
+ export { C as ChunkManifest, E as ExtractedKey } from '../types-DTUho6gi.js';
3
3
  import { AstroIntegrationLogger } from 'astro';
4
4
  import { Plugin } from 'vite';
5
- import '../types-Bzp6KxsI.js';
5
+ import '../types-DNRFj1XA.js';
6
6
 
7
7
  /**
8
8
  * Manifest state management for i18n extraction
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  getManifest,
3
3
  i18nVitePlugin
4
- } from "../chunk-JJEDOJBR.js";
4
+ } from "../chunk-FW6TPWMT.js";
5
5
  import "../chunk-4A5BKHFN.js";
6
6
  export {
7
7
  getManifest,
@@ -1,5 +1,5 @@
1
- import { a as ExtractionManifest } from './types-8uKCwFda.js';
2
- import { T as TranslationMeta, C as CompiledTranslations, R as RawTranslations } from './types-Bzp6KxsI.js';
1
+ import { a as ExtractionManifest } from './types-DTUho6gi.js';
2
+ import { T as TranslationMeta, C as CompiledTranslations, R as RawTranslations } from './types-DNRFj1XA.js';
3
3
 
4
4
  /**
5
5
  * Fallback behavior when translation is missing
@@ -30,6 +30,8 @@ declare class I18nSingleton {
30
30
  private mergedCache;
31
31
  private compiledCache;
32
32
  private hashCache;
33
+ private scriptCache;
34
+ private chunkCache;
33
35
  private manifestGetter;
34
36
  configure(config: I18nConfig): Promise<void>;
35
37
  isConfigured(): boolean;
@@ -44,6 +46,18 @@ declare class I18nSingleton {
44
46
  */
45
47
  getManifest(): ExtractionManifest;
46
48
  getHashes(locale: string): Record<string, string>;
49
+ /**
50
+ * Get the encoded chunk response body for a locale/chunk combination.
51
+ * Cached to avoid repeated JSON.stringify and encoding on each request.
52
+ * Returns undefined if chunk not found in manifest.
53
+ */
54
+ getChunkBody(locale: string, chunkName: string): Uint8Array | undefined;
55
+ /**
56
+ * Get the inline script for I18nScript component.
57
+ * Cached per locale, invalidated when translations change.
58
+ */
59
+ getClientScript(locale: string): string;
60
+ private createClientScript;
47
61
  /**
48
62
  * Clear translations for specified locale(s).
49
63
  * @param locales - Locale(s) to clear, or all if not specified
package/dist/index.d.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  import { AstroIntegration, MiddlewareHandler, APIContext } from 'astro';
2
- export { F as FallbackBehavior, I as I18nConfig, i as i18n } from './i18n-BGfrSNM8.js';
2
+ export { F as FallbackBehavior, I as I18nConfig, i as i18n } from './i18n-BBAAVYz_.js';
3
3
  import { ExcludePattern } from '@astroscope/excludes';
4
- export { R as RawTranslations, T as TranslationMeta } from './types-Bzp6KxsI.js';
5
- export { E as ExtractedKey, a as ExtractionManifest } from './types-8uKCwFda.js';
4
+ export { R as RawTranslations, T as TranslationMeta } from './types-DNRFj1XA.js';
5
+ export { E as ExtractedKey, a as ExtractionManifest } from './types-DTUho6gi.js';
6
6
 
7
7
  interface I18nOptions {
8
8
  }
package/dist/index.js CHANGED
@@ -1,10 +1,10 @@
1
1
  import {
2
2
  i18nVitePlugin
3
- } from "./chunk-JJEDOJBR.js";
3
+ } from "./chunk-FW6TPWMT.js";
4
4
  import {
5
5
  i18n,
6
6
  runWithContext
7
- } from "./chunk-QFAAGCP5.js";
7
+ } from "./chunk-ZKXMIGSF.js";
8
8
  import "./chunk-KNDYKTHE.js";
9
9
  import "./chunk-4A5BKHFN.js";
10
10
 
@@ -69,6 +69,13 @@ function createI18nChunkMiddleware() {
69
69
  return next();
70
70
  }
71
71
  const locale = path.slice(0, slashIdx);
72
+ const config = i18n.getConfig();
73
+ if (!config.locales.includes(locale)) {
74
+ return new Response("/* unknown locale */", {
75
+ status: 404,
76
+ headers: { "Content-Type": "application/javascript" }
77
+ });
78
+ }
72
79
  const rest = path.slice(slashIdx + 1);
73
80
  if (!rest.endsWith(".js")) {
74
81
  return next();
@@ -79,27 +86,13 @@ function createI18nChunkMiddleware() {
79
86
  return next();
80
87
  }
81
88
  const chunkName = withoutJs.slice(0, lastDotIdx);
82
- const keys = i18n.getManifest().chunks[chunkName];
83
- if (!keys) {
89
+ const body = i18n.getChunkBody(locale, chunkName);
90
+ if (!body) {
84
91
  return new Response(`/* chunk not found: ${chunkName} */`, {
85
92
  status: 404,
86
93
  headers: { "Content-Type": "application/javascript" }
87
94
  });
88
95
  }
89
- const translations = i18n.getTranslations(locale);
90
- const chunkTranslations = {};
91
- for (const key of keys) {
92
- if (translations[key]) {
93
- chunkTranslations[key] = translations[key];
94
- }
95
- }
96
- const js = `/* @astroscope/i18n chunk: ${chunkName} */
97
- (function() {
98
- var i = window.__i18n__;
99
- if (i) Object.assign(i.translations, ${JSON.stringify(chunkTranslations)});
100
- })();
101
- `;
102
- const body = new TextEncoder().encode(js);
103
96
  return new Response(body, {
104
97
  status: 200,
105
98
  headers: {
@@ -134,11 +127,12 @@ function detectLocale(request) {
134
127
  if (!acceptLanguage) return void 0;
135
128
  const preferred = acceptLanguage.split(",").map((part) => {
136
129
  const [lang, q = "q=1"] = part.trim().split(";");
130
+ const baseLang = lang?.split("-")[0]?.toLowerCase();
137
131
  return {
138
- lang: lang.split("-")[0].toLowerCase(),
132
+ lang: baseLang,
139
133
  q: parseFloat(q.replace("q=", ""))
140
134
  };
141
- }).sort((a, b) => b.q - a.q);
135
+ }).filter((p) => !!p.lang).sort((a, b) => b.q - a.q);
142
136
  for (const { lang } of preferred) {
143
137
  if (locales.includes(lang)) {
144
138
  return lang;
@@ -1,7 +1,7 @@
1
- import { a as I18nContext } from './i18n-BGfrSNM8.js';
2
- export { i as i18n } from './i18n-BGfrSNM8.js';
3
- export { I as I18nClientState } from './types-Bzp6KxsI.js';
4
- import './types-8uKCwFda.js';
1
+ import { a as I18nContext } from './i18n-BBAAVYz_.js';
2
+ export { i as i18n } from './i18n-BBAAVYz_.js';
3
+ export { I as I18nClientState } from './types-DNRFj1XA.js';
4
+ import './types-DTUho6gi.js';
5
5
 
6
6
  /**
7
7
  * Get the current i18n context
package/dist/internal.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  getContext,
3
3
  i18n
4
- } from "./chunk-QFAAGCP5.js";
4
+ } from "./chunk-ZKXMIGSF.js";
5
5
  import "./chunk-KNDYKTHE.js";
6
6
  export {
7
7
  getContext,
@@ -1,4 +1,4 @@
1
- import { a as TranslateFunction } from '../types-Bzp6KxsI.js';
1
+ import { a as TranslateFunction } from '../types-DNRFj1XA.js';
2
2
 
3
3
  /**
4
4
  * Get the current locale from request context.
package/dist/server/t.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  getContext,
3
3
  i18n
4
- } from "../chunk-QFAAGCP5.js";
4
+ } from "../chunk-ZKXMIGSF.js";
5
5
  import {
6
6
  normalizeMeta
7
7
  } from "../chunk-EMM6LRO2.js";
@@ -1,4 +1,4 @@
1
- import { I as I18nClientState } from './types-Bzp6KxsI.js';
1
+ import { I as I18nClientState } from './types-DNRFj1XA.js';
2
2
 
3
3
  declare global {
4
4
  interface Window {
@@ -32,6 +32,7 @@ type I18nClientState = {
32
32
  locale: string;
33
33
  hashes: Record<string, string>;
34
34
  translations: Record<string, string>;
35
+ imports: Record<string, string[]>;
35
36
  };
36
37
  /**
37
38
  * Translate function
@@ -1,4 +1,4 @@
1
- import { T as TranslationMeta } from './types-Bzp6KxsI.js';
1
+ import { T as TranslationMeta } from './types-DNRFj1XA.js';
2
2
 
3
3
  /**
4
4
  * Extracted metadata for a single translation key
@@ -13,12 +13,18 @@ type ExtractedKey = {
13
13
  * Chunk-to-keys mapping produced by Vite plugin
14
14
  */
15
15
  type ChunkManifest = Record<string, string[]>;
16
+ /**
17
+ * Chunk imports mapping: chunk name → array of imported chunk names that have i18n
18
+ * All direct and indirect descendants are flattened into a single array
19
+ */
20
+ type ImportsManifest = Record<string, string[]>;
16
21
  /**
17
22
  * Full extraction manifest
18
23
  */
19
24
  type ExtractionManifest = {
20
25
  keys: ExtractedKey[];
21
26
  chunks: ChunkManifest;
27
+ imports: ImportsManifest;
22
28
  };
23
29
 
24
30
  export type { ChunkManifest as C, ExtractedKey as E, ExtractionManifest as a };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astroscope/i18n",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "description": "i18n for Astro + React islands — automatic tree-shaking, parallel loading, any translation source",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -86,9 +86,11 @@
86
86
  "lint:fix": "eslint 'src/**/*.ts' --fix"
87
87
  },
88
88
  "dependencies": {
89
+ "@astrojs/compiler": "^2.12.2",
89
90
  "@babel/core": "^7.28.6",
90
91
  "@babel/preset-typescript": "^7.28.5",
91
92
  "@messageformat/core": "^3.4.0",
93
+ "glob": "^11.0.0",
92
94
  "magic-string": "^0.30.21"
93
95
  },
94
96
  "devDependencies": {
@@ -100,7 +102,7 @@
100
102
  "vite": "^6.4.1"
101
103
  },
102
104
  "peerDependencies": {
103
- "@astroscope/excludes": "^0.1.2",
105
+ "@astroscope/excludes": "^0.1.3",
104
106
  "astro": "^5.0.0"
105
107
  }
106
108
  }
@@ -1,29 +1,8 @@
1
1
  ---
2
- import { type I18nClientState, getContext, i18n } from '@astroscope/i18n/internal';
2
+ import { getContext, i18n } from '@astroscope/i18n/internal';
3
3
 
4
4
  const context = getContext();
5
-
6
- let script: string | null = null;
7
-
8
- if (context) {
9
- const { locale } = context;
10
- const { chunks } = i18n.getManifest();
11
- const hasChunks = Object.keys(chunks).length > 0;
12
-
13
- let state: I18nClientState;
14
-
15
- if (hasChunks) {
16
- const hashes = i18n.getHashes(locale) ?? {};
17
-
18
- state = { locale, hashes, translations: {} };
19
- } else {
20
- const raw = i18n.getTranslations(locale) ?? {};
21
-
22
- state = { locale, hashes: {}, translations: raw };
23
- }
24
-
25
- script = `window.__i18n__=${JSON.stringify(state)};`;
26
- }
5
+ const script = context ? i18n.getClientScript(context.locale) : null;
27
6
  ---
28
7
 
29
8
  {
@@ -1,193 +0,0 @@
1
- import {
2
- chunkIdToName
3
- } from "./chunk-4A5BKHFN.js";
4
-
5
- // src/extraction/manifest.ts
6
- import path from "path";
7
- var I18N_MANIFEST_GLOBAL_KEY = "__astroscope_i18n_manifest__";
8
- function getGlobalState() {
9
- const g = globalThis;
10
- if (!g[I18N_MANIFEST_GLOBAL_KEY]) {
11
- g[I18N_MANIFEST_GLOBAL_KEY] = {
12
- extractedKeys: [],
13
- chunkManifest: {},
14
- projectRoot: ""
15
- };
16
- }
17
- return g[I18N_MANIFEST_GLOBAL_KEY];
18
- }
19
- function getManifest() {
20
- const state = getGlobalState();
21
- const keys = state.extractedKeys.map((key) => ({
22
- ...key,
23
- // make file paths relative to project root for better readability
24
- // and avoiding leaking absolute paths from dev or ci environments
25
- file: state.projectRoot ? path.relative(state.projectRoot, key.file) : key.file
26
- }));
27
- return { keys, chunks: state.chunkManifest };
28
- }
29
-
30
- // src/extraction/vite-plugin.ts
31
- import fs from "fs";
32
- import path2 from "path";
33
- import MagicString from "magic-string";
34
- var VIRTUAL_MODULE_ID = "virtual:@astroscope/i18n/manifest";
35
- var isVirtualModuleId = (id) => id === VIRTUAL_MODULE_ID || id.startsWith(`${VIRTUAL_MODULE_ID}?`);
36
- var RESOLVED_VIRTUAL_MODULE_ID = `\0${VIRTUAL_MODULE_ID}`;
37
- var isResolvedVirtualModuleId = (id) => id === RESOLVED_VIRTUAL_MODULE_ID || id.startsWith(`\0${VIRTUAL_MODULE_ID}?`);
38
- var MANIFEST_FILE_NAME = "i18n-manifest.json";
39
- var INCLUDE_EXTENSIONS = [".ts", ".tsx", ".js", ".jsx", ".astro"];
40
- function i18nVitePlugin(options) {
41
- const { logger } = options;
42
- const extractedKeys = [];
43
- const fileToKeys = /* @__PURE__ */ new Map();
44
- const filesWithI18n = /* @__PURE__ */ new Set();
45
- let isBuild = false;
46
- let isSSR = false;
47
- return {
48
- name: "@astroscope/i18n/extract",
49
- enforce: "pre",
50
- configResolved(config) {
51
- const state = getGlobalState();
52
- state.projectRoot = config.root;
53
- state.extractedKeys = extractedKeys;
54
- isBuild = config.command === "build";
55
- isSSR = !!config.build.ssr;
56
- },
57
- resolveId(id) {
58
- if (isVirtualModuleId(id)) {
59
- return `\0${id}`;
60
- }
61
- },
62
- load(id) {
63
- if (isResolvedVirtualModuleId(id)) {
64
- if (isBuild) {
65
- return `
66
- import { readFileSync } from 'node:fs';
67
- import { fileURLToPath } from 'node:url';
68
- import { dirname, join } from 'node:path';
69
-
70
- const __dirname = dirname(fileURLToPath(import.meta.url));
71
- const manifestJson = JSON.parse(readFileSync(join(__dirname, '${MANIFEST_FILE_NAME}'), 'utf-8'));
72
-
73
- export const manifest = manifestJson;
74
- export function getManifest() { return manifestJson; }
75
- `;
76
- }
77
- return `
78
- import { getManifest as _getManifest } from '@astroscope/i18n/extraction';
79
- export const manifest = { keys: [], chunks: {} };
80
- export function getManifest() { return _getManifest(); }
81
- `;
82
- }
83
- },
84
- async transform(code, filename) {
85
- if (!INCLUDE_EXTENSIONS.some((ext) => filename.endsWith(ext))) {
86
- return null;
87
- }
88
- if (filename.includes("node_modules")) {
89
- return null;
90
- }
91
- if (!code.includes("@astroscope/i18n/t")) {
92
- return null;
93
- }
94
- filesWithI18n.add(filename);
95
- const fileKeys = [];
96
- const [{ transformAsync }, { i18nExtractPlugin }] = await Promise.all([
97
- import("@babel/core"),
98
- import("./babel-plugin-K2K4ZPM5.js")
99
- ]);
100
- const result = await transformAsync(code, {
101
- filename,
102
- sourceMaps: true,
103
- babelrc: false,
104
- configFile: false,
105
- parserOpts: {
106
- plugins: [
107
- ...filename.endsWith(".ts") || filename.endsWith(".tsx") ? ["typescript"] : [],
108
- ...filename.endsWith(".jsx") || filename.endsWith(".tsx") ? ["jsx"] : []
109
- ]
110
- },
111
- plugins: [
112
- [
113
- i18nExtractPlugin,
114
- {
115
- stripFallbacks: isBuild,
116
- // only strip in production
117
- onKeyExtracted: (key) => fileKeys.push(key),
118
- logger
119
- }
120
- ]
121
- ]
122
- });
123
- if (fileKeys.length > 0) {
124
- extractedKeys.push(...fileKeys);
125
- fileToKeys.set(
126
- filename,
127
- fileKeys.map((k) => k.key)
128
- );
129
- }
130
- if (isBuild && result?.code) {
131
- return { code: result.code, map: result.map };
132
- }
133
- return null;
134
- },
135
- generateBundle(_, bundle) {
136
- const state = getGlobalState();
137
- state.chunkManifest = {};
138
- for (const [fileName, chunk] of Object.entries(bundle)) {
139
- if (chunk.type === "chunk" && chunk.moduleIds) {
140
- const keys = /* @__PURE__ */ new Set();
141
- let hasI18nModule = false;
142
- for (const moduleId of chunk.moduleIds) {
143
- const moduleKeys = fileToKeys.get(moduleId);
144
- moduleKeys?.forEach((k) => keys.add(k));
145
- if (filesWithI18n.has(moduleId)) {
146
- hasI18nModule = true;
147
- }
148
- }
149
- const chunkName = chunkIdToName(fileName.replace(/\.js$/, ""));
150
- if (keys.size) {
151
- state.chunkManifest[chunkName] = Array.from(keys);
152
- }
153
- if (hasI18nModule) {
154
- const loaderCode = `
155
- /* @astroscope/i18n loader */
156
- if (typeof window !== 'undefined' && window.__i18n__) {
157
- const __i18n__ = window.__i18n__;
158
- const __i18n_hash__ = __i18n__.hashes[${JSON.stringify(chunkName)}];
159
- if (__i18n_hash__) {
160
- await import(\`/_i18n/\${__i18n__.locale}/${chunkName}.\${__i18n_hash__}.js\`);
161
- }
162
- }
163
- /* end @astroscope/i18n loader */
164
- `;
165
- const s = new MagicString(chunk.code);
166
- s.prepend(loaderCode);
167
- chunk.code = s.toString();
168
- if (chunk.map) {
169
- chunk.map = s.generateMap({ hires: true });
170
- }
171
- }
172
- }
173
- }
174
- logger.info(`manifest: ${Object.keys(state.chunkManifest).length} chunks, ${extractedKeys.length} keys`);
175
- },
176
- writeBundle(outputOptions) {
177
- if (!isSSR && outputOptions.dir) {
178
- const chunksDir = path2.resolve(outputOptions.dir, "..", "server", "chunks");
179
- const manifestPath = path2.join(chunksDir, MANIFEST_FILE_NAME);
180
- if (fs.existsSync(chunksDir)) {
181
- fs.writeFileSync(manifestPath, JSON.stringify(getManifest()));
182
- } else {
183
- logger.error(`server chunks directory not found, cannot write manifest to ${manifestPath}`);
184
- }
185
- }
186
- }
187
- };
188
- }
189
-
190
- export {
191
- getManifest,
192
- i18nVitePlugin
193
- };