@zachhandley/ez-i18n 0.1.2 → 0.1.5

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/dist/index.js CHANGED
@@ -1,21 +1,294 @@
1
+ // src/utils/translations.ts
2
+ import { glob } from "tinyglobby";
3
+ import * as path from "path";
4
+ import * as fs from "fs";
5
+ var CACHE_FILE = ".ez-i18n.json";
6
+ var CACHE_VERSION = 1;
7
+ var DEFAULT_I18N_DIR = "./public/i18n";
8
+ function detectPathType(input) {
9
+ if (Array.isArray(input)) return "array";
10
+ if (input.includes("*")) return "glob";
11
+ if (input.endsWith("/") || input.endsWith(path.sep)) return "folder";
12
+ return "file";
13
+ }
14
+ function isDirectory(filePath) {
15
+ try {
16
+ return fs.statSync(filePath).isDirectory();
17
+ } catch {
18
+ return false;
19
+ }
20
+ }
21
+ async function resolveTranslationPaths(input, projectRoot) {
22
+ const type = detectPathType(input);
23
+ let files = [];
24
+ switch (type) {
25
+ case "array":
26
+ for (const entry of input) {
27
+ const resolved = await resolveTranslationPaths(entry, projectRoot);
28
+ files.push(...resolved);
29
+ }
30
+ break;
31
+ case "glob":
32
+ files = await glob(input, {
33
+ cwd: projectRoot,
34
+ absolute: true
35
+ });
36
+ break;
37
+ case "folder": {
38
+ const folderPath = path.resolve(projectRoot, input.replace(/\/$/, ""));
39
+ files = await glob("**/*.json", {
40
+ cwd: folderPath,
41
+ absolute: true
42
+ });
43
+ break;
44
+ }
45
+ case "file":
46
+ default: {
47
+ const filePath = path.resolve(projectRoot, input);
48
+ if (isDirectory(filePath)) {
49
+ files = await glob("**/*.json", {
50
+ cwd: filePath,
51
+ absolute: true
52
+ });
53
+ } else {
54
+ files = [filePath];
55
+ }
56
+ break;
57
+ }
58
+ }
59
+ return [...new Set(files)].sort((a, b) => a.localeCompare(b));
60
+ }
61
+ async function autoDiscoverTranslations(baseDir, projectRoot, configuredLocales) {
62
+ const absoluteBaseDir = path.resolve(projectRoot, baseDir.replace(/\/$/, ""));
63
+ if (!isDirectory(absoluteBaseDir)) {
64
+ console.warn(`[ez-i18n] Translation directory not found: ${absoluteBaseDir}`);
65
+ return { locales: configuredLocales || [], translations: {} };
66
+ }
67
+ const translations = {};
68
+ const discoveredLocales = [];
69
+ const entries = fs.readdirSync(absoluteBaseDir, { withFileTypes: true });
70
+ for (const entry of entries) {
71
+ if (entry.isDirectory()) {
72
+ const locale = entry.name;
73
+ if (configuredLocales && configuredLocales.length > 0) {
74
+ if (!configuredLocales.includes(locale)) continue;
75
+ }
76
+ const localePath = path.join(absoluteBaseDir, locale);
77
+ const files = await glob("**/*.json", {
78
+ cwd: localePath,
79
+ absolute: true
80
+ });
81
+ if (files.length > 0) {
82
+ discoveredLocales.push(locale);
83
+ translations[locale] = files.sort((a, b) => a.localeCompare(b));
84
+ }
85
+ } else if (entry.isFile() && entry.name.endsWith(".json")) {
86
+ const locale = path.basename(entry.name, ".json");
87
+ if (configuredLocales && configuredLocales.length > 0) {
88
+ if (!configuredLocales.includes(locale)) continue;
89
+ }
90
+ const filePath = path.join(absoluteBaseDir, entry.name);
91
+ if (!translations[locale]) {
92
+ discoveredLocales.push(locale);
93
+ translations[locale] = [];
94
+ }
95
+ translations[locale].push(filePath);
96
+ }
97
+ }
98
+ const sortedLocales = [...new Set(discoveredLocales)].sort();
99
+ return { locales: sortedLocales, translations };
100
+ }
101
+ async function resolveTranslationsConfig(config, projectRoot, configuredLocales) {
102
+ if (!config) {
103
+ return autoDiscoverTranslations(DEFAULT_I18N_DIR, projectRoot, configuredLocales);
104
+ }
105
+ if (typeof config === "string") {
106
+ return autoDiscoverTranslations(config, projectRoot, configuredLocales);
107
+ }
108
+ const translations = {};
109
+ const locales = Object.keys(config);
110
+ for (const [locale, localePath] of Object.entries(config)) {
111
+ translations[locale] = await resolveTranslationPaths(localePath, projectRoot);
112
+ }
113
+ return { locales, translations };
114
+ }
115
+ function loadCache(projectRoot) {
116
+ const cachePath = path.join(projectRoot, CACHE_FILE);
117
+ try {
118
+ if (!fs.existsSync(cachePath)) return null;
119
+ const content = fs.readFileSync(cachePath, "utf-8");
120
+ const cache = JSON.parse(content);
121
+ if (cache.version !== CACHE_VERSION) return null;
122
+ return cache;
123
+ } catch {
124
+ return null;
125
+ }
126
+ }
127
+ function saveCache(projectRoot, discovered) {
128
+ const cachePath = path.join(projectRoot, CACHE_FILE);
129
+ const cache = {
130
+ version: CACHE_VERSION,
131
+ discovered,
132
+ lastScan: (/* @__PURE__ */ new Date()).toISOString()
133
+ };
134
+ try {
135
+ fs.writeFileSync(cachePath, JSON.stringify(cache, null, 2));
136
+ } catch (error) {
137
+ console.warn("[ez-i18n] Failed to write cache file:", error);
138
+ }
139
+ }
140
+ function isCacheValid(cache, projectRoot) {
141
+ for (const files of Object.values(cache.discovered)) {
142
+ for (const file of files) {
143
+ if (!fs.existsSync(file)) return false;
144
+ }
145
+ }
146
+ return true;
147
+ }
148
+ function toRelativeImport(absolutePath, projectRoot) {
149
+ const relativePath = path.relative(projectRoot, absolutePath);
150
+ const normalized = relativePath.replace(/\\/g, "/");
151
+ return normalized.startsWith(".") ? normalized : "./" + normalized;
152
+ }
153
+ function toGlobPattern(baseDir, projectRoot) {
154
+ const relativePath = path.relative(projectRoot, baseDir).replace(/\\/g, "/");
155
+ const normalized = relativePath.startsWith(".") ? relativePath : "./" + relativePath;
156
+ return `${normalized}/**/*.json`;
157
+ }
158
+ function getNamespaceFromPath(filePath, localeDir) {
159
+ const relative2 = path.relative(localeDir, filePath);
160
+ const withoutExt = relative2.replace(/\.json$/i, "");
161
+ const namespace = withoutExt.replace(/[\\/]/g, ".");
162
+ return namespace.replace(/\.index$/, "");
163
+ }
164
+ function generateNamespaceWrapperCode() {
165
+ return `
166
+ function __wrapWithNamespace(namespace, content) {
167
+ if (!namespace) return content;
168
+ const parts = namespace.split('.');
169
+ let result = content;
170
+ for (let i = parts.length - 1; i >= 0; i--) {
171
+ result = { [parts[i]]: result };
172
+ }
173
+ return result;
174
+ }`;
175
+ }
176
+
1
177
  // src/vite-plugin.ts
178
+ import * as path2 from "path";
2
179
  var VIRTUAL_CONFIG = "ez-i18n:config";
3
180
  var VIRTUAL_RUNTIME = "ez-i18n:runtime";
4
181
  var VIRTUAL_TRANSLATIONS = "ez-i18n:translations";
5
182
  var RESOLVED_PREFIX = "\0";
6
- function resolveConfig(config) {
7
- return {
8
- locales: config.locales,
9
- defaultLocale: config.defaultLocale,
10
- cookieName: config.cookieName ?? "ez-locale",
11
- translations: config.translations ?? {}
12
- };
13
- }
14
183
  function vitePlugin(config) {
15
- const resolved = resolveConfig(config);
184
+ let viteConfig;
185
+ let isDev = false;
186
+ let resolved;
187
+ let translationInfo = /* @__PURE__ */ new Map();
16
188
  return {
17
189
  name: "ez-i18n-vite",
18
190
  enforce: "pre",
191
+ configResolved(resolvedConfig) {
192
+ viteConfig = resolvedConfig;
193
+ isDev = resolvedConfig.command === "serve";
194
+ },
195
+ async buildStart() {
196
+ const projectRoot = viteConfig.root;
197
+ const isAutoDiscovery = !config.translations || typeof config.translations === "string";
198
+ const pathBasedNamespacing = config.pathBasedNamespacing ?? isAutoDiscovery;
199
+ const translationsBaseDir = typeof config.translations === "string" ? path2.resolve(projectRoot, config.translations.replace(/\/$/, "")) : path2.resolve(projectRoot, "./public/i18n");
200
+ let useCache = false;
201
+ if (!isDev) {
202
+ const cache = loadCache(projectRoot);
203
+ if (cache && isCacheValid(cache, projectRoot)) {
204
+ const localeBaseDirs = {};
205
+ for (const locale of Object.keys(cache.discovered)) {
206
+ localeBaseDirs[locale] = path2.join(translationsBaseDir, locale);
207
+ }
208
+ resolved = {
209
+ locales: config.locales || Object.keys(cache.discovered),
210
+ defaultLocale: config.defaultLocale,
211
+ cookieName: config.cookieName ?? "ez-locale",
212
+ translations: cache.discovered,
213
+ pathBasedNamespacing,
214
+ localeBaseDirs
215
+ };
216
+ useCache = true;
217
+ for (const [locale, files] of Object.entries(cache.discovered)) {
218
+ translationInfo.set(locale, {
219
+ locale,
220
+ files,
221
+ localeBaseDir: localeBaseDirs[locale]
222
+ });
223
+ }
224
+ }
225
+ }
226
+ if (!useCache) {
227
+ const { locales, translations } = await resolveTranslationsConfig(
228
+ config.translations,
229
+ projectRoot,
230
+ config.locales
231
+ );
232
+ const finalLocales = config.locales && config.locales.length > 0 ? config.locales : locales;
233
+ const localeBaseDirs = {};
234
+ for (const locale of finalLocales) {
235
+ if (isAutoDiscovery) {
236
+ localeBaseDirs[locale] = path2.join(translationsBaseDir, locale);
237
+ } else if (typeof config.translations === "object" && config.translations[locale]) {
238
+ const localeConfig = config.translations[locale];
239
+ if (typeof localeConfig === "string") {
240
+ const pathType = detectPathType(localeConfig);
241
+ if (pathType === "folder" || pathType === "file") {
242
+ const resolved2 = path2.resolve(projectRoot, localeConfig.replace(/\/$/, ""));
243
+ localeBaseDirs[locale] = pathType === "folder" ? resolved2 : path2.dirname(resolved2);
244
+ } else {
245
+ const baseDir = localeConfig.split("*")[0].replace(/\/$/, "");
246
+ localeBaseDirs[locale] = path2.resolve(projectRoot, baseDir);
247
+ }
248
+ } else {
249
+ localeBaseDirs[locale] = translationsBaseDir;
250
+ }
251
+ } else {
252
+ localeBaseDirs[locale] = translationsBaseDir;
253
+ }
254
+ }
255
+ resolved = {
256
+ locales: finalLocales,
257
+ defaultLocale: config.defaultLocale,
258
+ cookieName: config.cookieName ?? "ez-locale",
259
+ translations,
260
+ pathBasedNamespacing,
261
+ localeBaseDirs
262
+ };
263
+ for (const locale of finalLocales) {
264
+ const files = translations[locale] || [];
265
+ const info = {
266
+ locale,
267
+ files,
268
+ localeBaseDir: localeBaseDirs[locale]
269
+ };
270
+ if (isDev && config.translations) {
271
+ const localeConfig = typeof config.translations === "string" ? path2.join(config.translations, locale) : config.translations[locale];
272
+ if (localeConfig && typeof localeConfig === "string") {
273
+ const pathType = detectPathType(localeConfig);
274
+ if (pathType === "folder" || pathType === "glob") {
275
+ const basePath = pathType === "glob" ? localeConfig : toGlobPattern(path2.resolve(projectRoot, localeConfig), projectRoot);
276
+ info.globPattern = basePath;
277
+ }
278
+ }
279
+ }
280
+ translationInfo.set(locale, info);
281
+ }
282
+ if (!isDev && Object.keys(translations).length > 0) {
283
+ saveCache(projectRoot, translations);
284
+ }
285
+ }
286
+ if (!resolved.locales.includes(resolved.defaultLocale)) {
287
+ console.warn(
288
+ `[ez-i18n] defaultLocale "${resolved.defaultLocale}" not found in locales: [${resolved.locales.join(", ")}]`
289
+ );
290
+ }
291
+ },
19
292
  resolveId(id) {
20
293
  if (id === VIRTUAL_CONFIG || id === VIRTUAL_RUNTIME || id === VIRTUAL_TRANSLATIONS) {
21
294
  return RESOLVED_PREFIX + id;
@@ -81,21 +354,143 @@ export function t(key, params) {
81
354
  `;
82
355
  }
83
356
  if (id === RESOLVED_PREFIX + VIRTUAL_TRANSLATIONS) {
84
- const loaderEntries = Object.entries(resolved.translations).map(([locale, path]) => ` ${JSON.stringify(locale)}: () => import(${JSON.stringify(path)})`).join(",\n");
85
- return `
86
- /**
87
- * Translation loaders for ez-i18n
88
- * Auto-generated from config
89
- */
357
+ return isDev ? generateDevTranslationsModule(translationInfo, viteConfig.root, resolved.pathBasedNamespacing) : generateBuildTranslationsModule(translationInfo, viteConfig.root, resolved.pathBasedNamespacing);
358
+ }
359
+ return null;
360
+ },
361
+ // HMR support for dev mode
362
+ handleHotUpdate({ file, server }) {
363
+ if (!isDev) return;
364
+ for (const info of translationInfo.values()) {
365
+ if (info.files.includes(file)) {
366
+ const mod = server.moduleGraph.getModuleById(RESOLVED_PREFIX + VIRTUAL_TRANSLATIONS);
367
+ if (mod) {
368
+ server.moduleGraph.invalidateModule(mod);
369
+ server.ws.send({
370
+ type: "full-reload",
371
+ path: "*"
372
+ });
373
+ }
374
+ break;
375
+ }
376
+ }
377
+ },
378
+ configureServer(server) {
379
+ const watchedDirs = /* @__PURE__ */ new Set();
380
+ if (config.translations) {
381
+ if (typeof config.translations === "string") {
382
+ watchedDirs.add(path2.resolve(viteConfig.root, config.translations));
383
+ } else {
384
+ for (const localePath of Object.values(config.translations)) {
385
+ if (typeof localePath === "string") {
386
+ const pathType = detectPathType(localePath);
387
+ if (pathType === "folder") {
388
+ watchedDirs.add(path2.resolve(viteConfig.root, localePath));
389
+ } else if (pathType === "glob") {
390
+ const baseDir = localePath.split("*")[0].replace(/\/$/, "");
391
+ if (baseDir) {
392
+ watchedDirs.add(path2.resolve(viteConfig.root, baseDir));
393
+ }
394
+ }
395
+ } else if (Array.isArray(localePath)) {
396
+ for (const file of localePath) {
397
+ const dir = path2.dirname(path2.resolve(viteConfig.root, file));
398
+ watchedDirs.add(dir);
399
+ }
400
+ }
401
+ }
402
+ }
403
+ } else {
404
+ watchedDirs.add(path2.resolve(viteConfig.root, "./public/i18n"));
405
+ }
406
+ for (const dir of watchedDirs) {
407
+ server.watcher.add(dir);
408
+ }
409
+ }
410
+ };
411
+ }
412
+ function generateDevTranslationsModule(translationInfo, projectRoot, pathBasedNamespacing) {
413
+ const imports = [];
414
+ const loaderEntries = [];
415
+ imports.push(getDeepMergeCode());
416
+ if (pathBasedNamespacing) {
417
+ imports.push(generateNamespaceWrapperCode());
418
+ }
419
+ for (const [locale, info] of translationInfo) {
420
+ if (info.files.length === 0) {
421
+ loaderEntries.push(` ${JSON.stringify(locale)}: async () => ({})`);
422
+ } else if (info.globPattern && pathBasedNamespacing && info.localeBaseDir) {
423
+ const varName = `__${locale}Modules`;
424
+ const localeBaseDir = info.localeBaseDir.replace(/\\/g, "/");
425
+ imports.push(
426
+ `const ${varName} = import.meta.glob(${JSON.stringify(info.globPattern)}, { eager: true, import: 'default' });`
427
+ );
428
+ loaderEntries.push(` ${JSON.stringify(locale)}: async () => {
429
+ const entries = Object.entries(${varName});
430
+ if (entries.length === 0) return {};
431
+ const localeBaseDir = ${JSON.stringify(localeBaseDir)};
432
+ const wrapped = entries.map(([filePath, content]) => {
433
+ // Extract relative path from locale base dir
434
+ const relativePath = filePath.replace(localeBaseDir + '/', '').replace(/\\.json$/i, '');
435
+ const namespace = relativePath.replace(/[\\/]/g, '.').replace(/\\.index$/, '');
436
+ return __wrapWithNamespace(namespace, content);
437
+ });
438
+ return __deepMerge({}, ...wrapped);
439
+ }`);
440
+ } else if (info.globPattern) {
441
+ const varName = `__${locale}Modules`;
442
+ imports.push(
443
+ `const ${varName} = import.meta.glob(${JSON.stringify(info.globPattern)}, { eager: true, import: 'default' });`
444
+ );
445
+ loaderEntries.push(` ${JSON.stringify(locale)}: async () => {
446
+ const modules = Object.values(${varName});
447
+ if (modules.length === 0) return {};
448
+ if (modules.length === 1) return modules[0];
449
+ return __deepMerge({}, ...modules);
450
+ }`);
451
+ } else if (info.files.length === 1) {
452
+ const relativePath = toRelativeImport(info.files[0], projectRoot);
453
+ if (pathBasedNamespacing && info.localeBaseDir) {
454
+ const namespace = getNamespaceFromPath(info.files[0], info.localeBaseDir);
455
+ loaderEntries.push(
456
+ ` ${JSON.stringify(locale)}: () => import(${JSON.stringify(relativePath)}).then(m => __wrapWithNamespace(${JSON.stringify(namespace)}, m.default ?? m))`
457
+ );
458
+ } else {
459
+ loaderEntries.push(
460
+ ` ${JSON.stringify(locale)}: () => import(${JSON.stringify(relativePath)}).then(m => m.default ?? m)`
461
+ );
462
+ }
463
+ } else {
464
+ if (pathBasedNamespacing && info.localeBaseDir) {
465
+ const fileEntries = info.files.map((f) => {
466
+ const relativePath = toRelativeImport(f, projectRoot);
467
+ const namespace = getNamespaceFromPath(f, info.localeBaseDir);
468
+ return `{ path: ${JSON.stringify(relativePath)}, namespace: ${JSON.stringify(namespace)} }`;
469
+ });
470
+ loaderEntries.push(` ${JSON.stringify(locale)}: async () => {
471
+ const fileInfos = [${fileEntries.join(", ")}];
472
+ const modules = await Promise.all(fileInfos.map(f => import(f.path)));
473
+ const wrapped = modules.map((m, i) => __wrapWithNamespace(fileInfos[i].namespace, m.default ?? m));
474
+ return __deepMerge({}, ...wrapped);
475
+ }`);
476
+ } else {
477
+ const fileImports = info.files.map((f) => `import(${JSON.stringify(toRelativeImport(f, projectRoot))})`).join(", ");
478
+ loaderEntries.push(` ${JSON.stringify(locale)}: async () => {
479
+ const modules = await Promise.all([${fileImports}]);
480
+ const contents = modules.map(m => m.default ?? m);
481
+ if (contents.length === 1) return contents[0];
482
+ return __deepMerge({}, ...contents);
483
+ }`);
484
+ }
485
+ }
486
+ }
487
+ return `
488
+ ${imports.join("\n")}
489
+
90
490
  export const translationLoaders = {
91
- ${loaderEntries}
491
+ ${loaderEntries.join(",\n")}
92
492
  };
93
493
 
94
- /**
95
- * Load translations for a specific locale
96
- * @param locale - Locale code to load translations for
97
- * @returns Translations object or empty object if not found
98
- */
99
494
  export async function loadTranslations(locale) {
100
495
  const loader = translationLoaders[locale];
101
496
  if (!loader) {
@@ -106,8 +501,7 @@ export async function loadTranslations(locale) {
106
501
  }
107
502
 
108
503
  try {
109
- const mod = await loader();
110
- return mod.default ?? mod;
504
+ return await loader();
111
505
  } catch (error) {
112
506
  if (import.meta.env.DEV) {
113
507
  console.error('[ez-i18n] Failed to load translations for locale:', locale, error);
@@ -116,9 +510,107 @@ export async function loadTranslations(locale) {
116
510
  }
117
511
  }
118
512
  `;
513
+ }
514
+ function generateBuildTranslationsModule(translationInfo, projectRoot, pathBasedNamespacing) {
515
+ const loaderEntries = [];
516
+ let needsDeepMerge = false;
517
+ let needsNamespaceWrapper = false;
518
+ for (const [locale, info] of translationInfo) {
519
+ if (info.files.length === 0) {
520
+ loaderEntries.push(` ${JSON.stringify(locale)}: async () => ({})`);
521
+ } else if (info.files.length === 1) {
522
+ const relativePath = toRelativeImport(info.files[0], projectRoot);
523
+ if (pathBasedNamespacing && info.localeBaseDir) {
524
+ needsNamespaceWrapper = true;
525
+ const namespace = getNamespaceFromPath(info.files[0], info.localeBaseDir);
526
+ loaderEntries.push(
527
+ ` ${JSON.stringify(locale)}: () => import(${JSON.stringify(relativePath)}).then(m => __wrapWithNamespace(${JSON.stringify(namespace)}, m.default ?? m))`
528
+ );
529
+ } else {
530
+ loaderEntries.push(
531
+ ` ${JSON.stringify(locale)}: () => import(${JSON.stringify(relativePath)}).then(m => m.default ?? m)`
532
+ );
533
+ }
534
+ } else {
535
+ needsDeepMerge = true;
536
+ if (pathBasedNamespacing && info.localeBaseDir) {
537
+ needsNamespaceWrapper = true;
538
+ const fileEntries = info.files.map((f) => {
539
+ const relativePath = toRelativeImport(f, projectRoot);
540
+ const namespace = getNamespaceFromPath(f, info.localeBaseDir);
541
+ return `{ path: ${JSON.stringify(relativePath)}, namespace: ${JSON.stringify(namespace)} }`;
542
+ });
543
+ loaderEntries.push(` ${JSON.stringify(locale)}: async () => {
544
+ const fileInfos = [${fileEntries.join(", ")}];
545
+ const modules = await Promise.all(fileInfos.map(f => import(f.path)));
546
+ const wrapped = modules.map((m, i) => __wrapWithNamespace(fileInfos[i].namespace, m.default ?? m));
547
+ return __deepMerge({}, ...wrapped);
548
+ }`);
549
+ } else {
550
+ const fileImports = info.files.map((f) => `import(${JSON.stringify(toRelativeImport(f, projectRoot))})`).join(", ");
551
+ loaderEntries.push(` ${JSON.stringify(locale)}: async () => {
552
+ const modules = await Promise.all([${fileImports}]);
553
+ const contents = modules.map(m => m.default ?? m);
554
+ return __deepMerge({}, ...contents);
555
+ }`);
119
556
  }
120
- return null;
121
557
  }
558
+ }
559
+ const helperCode = [
560
+ needsDeepMerge ? getDeepMergeCode() : "",
561
+ needsNamespaceWrapper ? generateNamespaceWrapperCode() : ""
562
+ ].filter(Boolean).join("\n");
563
+ return `
564
+ ${helperCode}
565
+
566
+ export const translationLoaders = {
567
+ ${loaderEntries.join(",\n")}
568
+ };
569
+
570
+ export async function loadTranslations(locale) {
571
+ const loader = translationLoaders[locale];
572
+ if (!loader) {
573
+ return {};
574
+ }
575
+
576
+ try {
577
+ return await loader();
578
+ } catch (error) {
579
+ console.error('[ez-i18n] Failed to load translations:', locale, error);
580
+ return {};
581
+ }
582
+ }
583
+ `;
584
+ }
585
+ function getDeepMergeCode() {
586
+ return `
587
+ function __deepMerge(target, ...sources) {
588
+ const FORBIDDEN = new Set(['__proto__', 'constructor', 'prototype']);
589
+ const result = { ...target };
590
+ for (const source of sources) {
591
+ if (!source || typeof source !== 'object') continue;
592
+ for (const key of Object.keys(source)) {
593
+ if (FORBIDDEN.has(key)) continue;
594
+ const tv = result[key], sv = source[key];
595
+ if (sv && typeof sv === 'object' && !Array.isArray(sv) && tv && typeof tv === 'object' && !Array.isArray(tv)) {
596
+ result[key] = __deepMerge(tv, sv);
597
+ } else {
598
+ result[key] = sv;
599
+ }
600
+ }
601
+ }
602
+ return result;
603
+ }`;
604
+ }
605
+ function resolveConfig(config) {
606
+ const isAutoDiscovery = !config.translations || typeof config.translations === "string";
607
+ return {
608
+ locales: config.locales || [],
609
+ defaultLocale: config.defaultLocale,
610
+ cookieName: config.cookieName ?? "ez-locale",
611
+ translations: {},
612
+ pathBasedNamespacing: config.pathBasedNamespacing ?? isAutoDiscovery,
613
+ localeBaseDirs: {}
122
614
  };
123
615
  }
124
616
 
@@ -1,5 +1,5 @@
1
1
  import { setLocale } from './index.js';
2
- import { T as TranslateFunction } from '../types-DwCG8sp8.js';
2
+ import { T as TranslateFunction } from '../types-Cd9e7Lkc.js';
3
3
  import 'nanostores';
4
4
 
5
5
  /**
@@ -1,7 +1,7 @@
1
1
  import * as vue from 'vue';
2
2
  import { Plugin } from 'vue';
3
3
  import { setLocale } from './index.js';
4
- import { T as TranslateFunction } from '../types-DwCG8sp8.js';
4
+ import { T as TranslateFunction } from '../types-Cd9e7Lkc.js';
5
5
  import 'nanostores';
6
6
 
7
7
  /**
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Translation path for a single locale:
3
+ * - Single file: `./src/i18n/en.json`
4
+ * - Folder: `./src/i18n/en/` (auto-discover all JSONs inside)
5
+ * - Glob: `./src/i18n/en/**.json` (recursive)
6
+ * - Array: `['./common.json', './auth.json']`
7
+ */
8
+ type LocaleTranslationPath = string | string[];
9
+ /**
10
+ * Translation config can be:
11
+ * - A base directory string (auto-discovers locale folders): './public/i18n/'
12
+ * - Per-locale mapping: { en: './src/i18n/en/', es: './src/i18n/es.json' }
13
+ */
14
+ type TranslationsConfig = string | Record<string, LocaleTranslationPath>;
15
+ /**
16
+ * Configuration for ez-i18n Astro integration
17
+ */
18
+ interface EzI18nConfig {
19
+ /**
20
+ * List of supported locale codes (e.g., ['en', 'es', 'fr'])
21
+ * Optional if using directory-based auto-discovery - locales will be
22
+ * detected from folder names in the translations directory.
23
+ */
24
+ locales?: string[];
25
+ /**
26
+ * Default locale to use when no preference is detected.
27
+ * Required - this tells us what to fall back to.
28
+ */
29
+ defaultLocale: string;
30
+ /**
31
+ * Cookie name for storing locale preference
32
+ * @default 'ez-locale'
33
+ */
34
+ cookieName?: string;
35
+ /**
36
+ * Translation file paths configuration.
37
+ * Paths are relative to your project root.
38
+ *
39
+ * Can be:
40
+ * - A base directory (auto-discovers locale folders):
41
+ * translations: './public/i18n/'
42
+ * → Scans for en/, es/, fr/ folders and their JSON files
43
+ * → Auto-populates `locales` from discovered folders
44
+ *
45
+ * - Per-locale mapping with flexible path types:
46
+ * translations: {
47
+ * en: './src/i18n/en.json', // single file
48
+ * es: './src/i18n/es/', // folder (all JSONs)
49
+ * fr: './src/i18n/fr/**.json', // glob pattern
50
+ * de: ['./common.json', './auth.json'] // array of files
51
+ * }
52
+ *
53
+ * If not specified, auto-discovers from ./public/i18n/
54
+ */
55
+ translations?: TranslationsConfig;
56
+ /**
57
+ * Derive namespace from file path relative to locale folder.
58
+ *
59
+ * When enabled:
60
+ * - `en/auth/login.json` with `{ "title": "..." }` → `$t('auth.login.title')`
61
+ * - `en/common.json` with `{ "actions": {...} }` → `$t('common.actions.save')`
62
+ *
63
+ * The file path (minus locale folder and .json extension) becomes the key prefix.
64
+ *
65
+ * @default true when using folder-based translations config
66
+ */
67
+ pathBasedNamespacing?: boolean;
68
+ }
69
+ /**
70
+ * Cache file structure (.ez-i18n.json)
71
+ * Used to speed up subsequent builds by caching discovered translations
72
+ */
73
+ interface TranslationCache {
74
+ version: number;
75
+ /** Discovered locale → file paths mapping */
76
+ discovered: Record<string, string[]>;
77
+ /** ISO timestamp of last scan */
78
+ lastScan: string;
79
+ }
80
+ /**
81
+ * Translation function type
82
+ */
83
+ type TranslateFunction = (key: string, params?: Record<string, string | number>) => string;
84
+ /**
85
+ * Augment Astro's locals type
86
+ */
87
+ declare global {
88
+ namespace App {
89
+ interface Locals {
90
+ /** Current locale code */
91
+ locale: string;
92
+ /** Loaded translations for the current locale */
93
+ translations: Record<string, unknown>;
94
+ }
95
+ }
96
+ }
97
+
98
+ export type { EzI18nConfig as E, LocaleTranslationPath as L, TranslateFunction as T, TranslationsConfig as a, TranslationCache as b };