@zachhandley/ez-i18n 0.1.2 → 0.1.3

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 CHANGED
@@ -59,6 +59,80 @@ export default defineConfig({
59
59
 
60
60
  Create similar files for each locale: `src/i18n/en.json`, `src/i18n/es.json`, etc.
61
61
 
62
+ ### Multi-File Translations
63
+
64
+ ez-i18n supports flexible translation file organization:
65
+
66
+ #### Auto-Discovery (Zero Config)
67
+
68
+ Just put your files in `public/i18n/` and ez-i18n will discover them automatically:
69
+
70
+ ```
71
+ public/i18n/
72
+ en/
73
+ common.json
74
+ auth.json
75
+ es/
76
+ common.json
77
+ auth.json
78
+ ```
79
+
80
+ ```typescript
81
+ // astro.config.ts - locales auto-discovered from folder names!
82
+ ezI18n({
83
+ defaultLocale: 'en',
84
+ // No locales or translations needed - auto-discovered
85
+ })
86
+ ```
87
+
88
+ #### Base Directory
89
+
90
+ Point to a folder and locales are discovered from subfolders:
91
+
92
+ ```typescript
93
+ ezI18n({
94
+ defaultLocale: 'en',
95
+ translations: './src/i18n/', // Discovers en/, es/, fr/ folders
96
+ })
97
+ ```
98
+
99
+ #### Per-Locale with Multiple Formats
100
+
101
+ Mix and match different formats per locale:
102
+
103
+ ```typescript
104
+ ezI18n({
105
+ locales: ['en', 'es', 'fr', 'de'],
106
+ defaultLocale: 'en',
107
+ translations: {
108
+ en: './src/i18n/en.json', // Single file
109
+ es: './src/i18n/es/', // Folder (all JSONs merged)
110
+ fr: './src/i18n/fr/**/*.json', // Glob pattern
111
+ de: ['./src/i18n/de/common.json', // Array of files
112
+ './src/i18n/de/auth.json'],
113
+ },
114
+ })
115
+ ```
116
+
117
+ #### Merge Order
118
+
119
+ When using multiple files per locale, files are merged **alphabetically by filename**. Later files override earlier ones for conflicting keys.
120
+
121
+ ```
122
+ en/
123
+ 01-common.json # Loaded first
124
+ 02-features.json # Loaded second, overrides common
125
+ 99-overrides.json # Loaded last, highest priority
126
+ ```
127
+
128
+ #### Cache File
129
+
130
+ A `.ez-i18n.json` cache file is generated to speed up subsequent builds. Add it to `.gitignore`:
131
+
132
+ ```gitignore
133
+ .ez-i18n.json
134
+ ```
135
+
62
136
  ### Layout Setup
63
137
 
64
138
  Add the `EzI18nHead` component to your layout's head for automatic hydration:
@@ -172,6 +246,9 @@ function MyComponent() {
172
246
  - **Vue integration** - Global `$t()`, `$locale`, `$setLocale` in templates
173
247
  - **React integration** - `useI18n()` hook for React components
174
248
  - **Middleware included** - Auto-detects locale from cookie, query param, or Accept-Language header
249
+ - **Multi-file support** - Organize translations in folders, use globs, or arrays
250
+ - **Auto-discovery** - Automatic locale detection from folder structure
251
+ - **HMR in dev** - Hot reload translation changes without restart
175
252
 
176
253
  ## Locale Detection Priority
177
254
 
@@ -188,10 +265,16 @@ Astro integration function.
188
265
 
189
266
  | Option | Type | Required | Description |
190
267
  |--------|------|----------|-------------|
191
- | `locales` | `string[]` | Yes | Supported locale codes |
268
+ | `locales` | `string[]` | No | Supported locale codes (auto-discovered if not provided) |
192
269
  | `defaultLocale` | `string` | Yes | Fallback locale |
193
270
  | `cookieName` | `string` | No | Cookie name (default: `'ez-locale'`) |
194
- | `translations` | `Record<string, string>` | No | Paths to translation files (auto-loaded) |
271
+ | `translations` | `string \| Record<string, TranslationPath>` | No | Base directory or per-locale paths (default: `./public/i18n/`) |
272
+
273
+ **TranslationPath** can be:
274
+ - Single file: `'./src/i18n/en.json'`
275
+ - Folder: `'./src/i18n/en/'`
276
+ - Glob: `'./src/i18n/en/**/*.json'`
277
+ - Array: `['./common.json', './auth.json']`
195
278
 
196
279
  ### `EzI18nHead`
197
280
 
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { AstroIntegration } from 'astro';
2
- import { E as EzI18nConfig } from './types-DwCG8sp8.js';
3
- export { T as TranslateFunction } from './types-DwCG8sp8.js';
2
+ import { E as EzI18nConfig } from './types-CHyDGt_C.js';
3
+ export { T as TranslateFunction } from './types-CHyDGt_C.js';
4
4
 
5
5
  /**
6
6
  * ez-i18n Astro integration
package/dist/index.js CHANGED
@@ -1,21 +1,235 @@
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
+
1
159
  // src/vite-plugin.ts
160
+ import * as path2 from "path";
2
161
  var VIRTUAL_CONFIG = "ez-i18n:config";
3
162
  var VIRTUAL_RUNTIME = "ez-i18n:runtime";
4
163
  var VIRTUAL_TRANSLATIONS = "ez-i18n:translations";
5
164
  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
165
  function vitePlugin(config) {
15
- const resolved = resolveConfig(config);
166
+ let viteConfig;
167
+ let isDev = false;
168
+ let resolved;
169
+ let translationInfo = /* @__PURE__ */ new Map();
16
170
  return {
17
171
  name: "ez-i18n-vite",
18
172
  enforce: "pre",
173
+ configResolved(resolvedConfig) {
174
+ viteConfig = resolvedConfig;
175
+ isDev = resolvedConfig.command === "serve";
176
+ },
177
+ async buildStart() {
178
+ const projectRoot = viteConfig.root;
179
+ let useCache = false;
180
+ if (!isDev) {
181
+ const cache = loadCache(projectRoot);
182
+ if (cache && isCacheValid(cache, projectRoot)) {
183
+ resolved = {
184
+ locales: config.locales || Object.keys(cache.discovered),
185
+ defaultLocale: config.defaultLocale,
186
+ cookieName: config.cookieName ?? "ez-locale",
187
+ translations: cache.discovered
188
+ };
189
+ useCache = true;
190
+ for (const [locale, files] of Object.entries(cache.discovered)) {
191
+ translationInfo.set(locale, { locale, files });
192
+ }
193
+ }
194
+ }
195
+ if (!useCache) {
196
+ const { locales, translations } = await resolveTranslationsConfig(
197
+ config.translations,
198
+ projectRoot,
199
+ config.locales
200
+ );
201
+ const finalLocales = config.locales && config.locales.length > 0 ? config.locales : locales;
202
+ resolved = {
203
+ locales: finalLocales,
204
+ defaultLocale: config.defaultLocale,
205
+ cookieName: config.cookieName ?? "ez-locale",
206
+ translations
207
+ };
208
+ for (const locale of finalLocales) {
209
+ const files = translations[locale] || [];
210
+ const info = { locale, files };
211
+ if (isDev && config.translations) {
212
+ const localeConfig = typeof config.translations === "string" ? path2.join(config.translations, locale) : config.translations[locale];
213
+ if (localeConfig && typeof localeConfig === "string") {
214
+ const pathType = detectPathType(localeConfig);
215
+ if (pathType === "folder" || pathType === "glob") {
216
+ const basePath = pathType === "glob" ? localeConfig : toGlobPattern(path2.resolve(projectRoot, localeConfig), projectRoot);
217
+ info.globPattern = basePath;
218
+ }
219
+ }
220
+ }
221
+ translationInfo.set(locale, info);
222
+ }
223
+ if (!isDev && Object.keys(translations).length > 0) {
224
+ saveCache(projectRoot, translations);
225
+ }
226
+ }
227
+ if (!resolved.locales.includes(resolved.defaultLocale)) {
228
+ console.warn(
229
+ `[ez-i18n] defaultLocale "${resolved.defaultLocale}" not found in locales: [${resolved.locales.join(", ")}]`
230
+ );
231
+ }
232
+ },
19
233
  resolveId(id) {
20
234
  if (id === VIRTUAL_CONFIG || id === VIRTUAL_RUNTIME || id === VIRTUAL_TRANSLATIONS) {
21
235
  return RESOLVED_PREFIX + id;
@@ -81,21 +295,101 @@ export function t(key, params) {
81
295
  `;
82
296
  }
83
297
  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
- */
298
+ return isDev ? generateDevTranslationsModule(translationInfo, viteConfig.root) : generateBuildTranslationsModule(translationInfo, viteConfig.root);
299
+ }
300
+ return null;
301
+ },
302
+ // HMR support for dev mode
303
+ handleHotUpdate({ file, server }) {
304
+ if (!isDev) return;
305
+ for (const info of translationInfo.values()) {
306
+ if (info.files.includes(file)) {
307
+ const mod = server.moduleGraph.getModuleById(RESOLVED_PREFIX + VIRTUAL_TRANSLATIONS);
308
+ if (mod) {
309
+ server.moduleGraph.invalidateModule(mod);
310
+ server.ws.send({
311
+ type: "full-reload",
312
+ path: "*"
313
+ });
314
+ }
315
+ break;
316
+ }
317
+ }
318
+ },
319
+ configureServer(server) {
320
+ const watchedDirs = /* @__PURE__ */ new Set();
321
+ if (config.translations) {
322
+ if (typeof config.translations === "string") {
323
+ watchedDirs.add(path2.resolve(viteConfig.root, config.translations));
324
+ } else {
325
+ for (const localePath of Object.values(config.translations)) {
326
+ if (typeof localePath === "string") {
327
+ const pathType = detectPathType(localePath);
328
+ if (pathType === "folder") {
329
+ watchedDirs.add(path2.resolve(viteConfig.root, localePath));
330
+ } else if (pathType === "glob") {
331
+ const baseDir = localePath.split("*")[0].replace(/\/$/, "");
332
+ if (baseDir) {
333
+ watchedDirs.add(path2.resolve(viteConfig.root, baseDir));
334
+ }
335
+ }
336
+ } else if (Array.isArray(localePath)) {
337
+ for (const file of localePath) {
338
+ const dir = path2.dirname(path2.resolve(viteConfig.root, file));
339
+ watchedDirs.add(dir);
340
+ }
341
+ }
342
+ }
343
+ }
344
+ } else {
345
+ watchedDirs.add(path2.resolve(viteConfig.root, "./public/i18n"));
346
+ }
347
+ for (const dir of watchedDirs) {
348
+ server.watcher.add(dir);
349
+ }
350
+ }
351
+ };
352
+ }
353
+ function generateDevTranslationsModule(translationInfo, projectRoot) {
354
+ const imports = [];
355
+ const loaderEntries = [];
356
+ imports.push(getDeepMergeCode());
357
+ for (const [locale, info] of translationInfo) {
358
+ if (info.files.length === 0) {
359
+ loaderEntries.push(` ${JSON.stringify(locale)}: async () => ({})`);
360
+ } else if (info.globPattern) {
361
+ const varName = `__${locale}Modules`;
362
+ imports.push(
363
+ `const ${varName} = import.meta.glob(${JSON.stringify(info.globPattern)}, { eager: true, import: 'default' });`
364
+ );
365
+ loaderEntries.push(` ${JSON.stringify(locale)}: async () => {
366
+ const modules = Object.values(${varName});
367
+ if (modules.length === 0) return {};
368
+ if (modules.length === 1) return modules[0];
369
+ return __deepMerge({}, ...modules);
370
+ }`);
371
+ } else if (info.files.length === 1) {
372
+ const relativePath = toRelativeImport(info.files[0], projectRoot);
373
+ loaderEntries.push(
374
+ ` ${JSON.stringify(locale)}: () => import(${JSON.stringify(relativePath)}).then(m => m.default ?? m)`
375
+ );
376
+ } else {
377
+ const fileImports = info.files.map((f) => `import(${JSON.stringify(toRelativeImport(f, projectRoot))})`).join(", ");
378
+ loaderEntries.push(` ${JSON.stringify(locale)}: async () => {
379
+ const modules = await Promise.all([${fileImports}]);
380
+ const contents = modules.map(m => m.default ?? m);
381
+ if (contents.length === 1) return contents[0];
382
+ return __deepMerge({}, ...contents);
383
+ }`);
384
+ }
385
+ }
386
+ return `
387
+ ${imports.join("\n")}
388
+
90
389
  export const translationLoaders = {
91
- ${loaderEntries}
390
+ ${loaderEntries.join(",\n")}
92
391
  };
93
392
 
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
393
  export async function loadTranslations(locale) {
100
394
  const loader = translationLoaders[locale];
101
395
  if (!loader) {
@@ -106,8 +400,7 @@ export async function loadTranslations(locale) {
106
400
  }
107
401
 
108
402
  try {
109
- const mod = await loader();
110
- return mod.default ?? mod;
403
+ return await loader();
111
404
  } catch (error) {
112
405
  if (import.meta.env.DEV) {
113
406
  console.error('[ez-i18n] Failed to load translations for locale:', locale, error);
@@ -116,9 +409,77 @@ export async function loadTranslations(locale) {
116
409
  }
117
410
  }
118
411
  `;
412
+ }
413
+ function generateBuildTranslationsModule(translationInfo, projectRoot) {
414
+ const loaderEntries = [];
415
+ let needsDeepMerge = false;
416
+ for (const [locale, info] of translationInfo) {
417
+ if (info.files.length === 0) {
418
+ loaderEntries.push(` ${JSON.stringify(locale)}: async () => ({})`);
419
+ } else if (info.files.length === 1) {
420
+ const relativePath = toRelativeImport(info.files[0], projectRoot);
421
+ loaderEntries.push(
422
+ ` ${JSON.stringify(locale)}: () => import(${JSON.stringify(relativePath)}).then(m => m.default ?? m)`
423
+ );
424
+ } else {
425
+ needsDeepMerge = true;
426
+ const fileImports = info.files.map((f) => `import(${JSON.stringify(toRelativeImport(f, projectRoot))})`).join(", ");
427
+ loaderEntries.push(` ${JSON.stringify(locale)}: async () => {
428
+ const modules = await Promise.all([${fileImports}]);
429
+ const contents = modules.map(m => m.default ?? m);
430
+ return __deepMerge({}, ...contents);
431
+ }`);
432
+ }
433
+ }
434
+ const deepMergeCode = needsDeepMerge ? getDeepMergeCode() : "";
435
+ return `
436
+ ${deepMergeCode}
437
+
438
+ export const translationLoaders = {
439
+ ${loaderEntries.join(",\n")}
440
+ };
441
+
442
+ export async function loadTranslations(locale) {
443
+ const loader = translationLoaders[locale];
444
+ if (!loader) {
445
+ return {};
446
+ }
447
+
448
+ try {
449
+ return await loader();
450
+ } catch (error) {
451
+ console.error('[ez-i18n] Failed to load translations:', locale, error);
452
+ return {};
453
+ }
454
+ }
455
+ `;
456
+ }
457
+ function getDeepMergeCode() {
458
+ return `
459
+ function __deepMerge(target, ...sources) {
460
+ const FORBIDDEN = new Set(['__proto__', 'constructor', 'prototype']);
461
+ const result = { ...target };
462
+ for (const source of sources) {
463
+ if (!source || typeof source !== 'object') continue;
464
+ for (const key of Object.keys(source)) {
465
+ if (FORBIDDEN.has(key)) continue;
466
+ const tv = result[key], sv = source[key];
467
+ if (sv && typeof sv === 'object' && !Array.isArray(sv) && tv && typeof tv === 'object' && !Array.isArray(tv)) {
468
+ result[key] = __deepMerge(tv, sv);
469
+ } else {
470
+ result[key] = sv;
119
471
  }
120
- return null;
121
472
  }
473
+ }
474
+ return result;
475
+ }`;
476
+ }
477
+ function resolveConfig(config) {
478
+ return {
479
+ locales: config.locales || [],
480
+ defaultLocale: config.defaultLocale,
481
+ cookieName: config.cookieName ?? "ez-locale",
482
+ translations: {}
122
483
  };
123
484
  }
124
485
 
@@ -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-CHyDGt_C.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-CHyDGt_C.js';
5
5
  import 'nanostores';
6
6
 
7
7
  /**
@@ -0,0 +1,86 @@
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
+ /**
58
+ * Cache file structure (.ez-i18n.json)
59
+ * Used to speed up subsequent builds by caching discovered translations
60
+ */
61
+ interface TranslationCache {
62
+ version: number;
63
+ /** Discovered locale → file paths mapping */
64
+ discovered: Record<string, string[]>;
65
+ /** ISO timestamp of last scan */
66
+ lastScan: string;
67
+ }
68
+ /**
69
+ * Translation function type
70
+ */
71
+ type TranslateFunction = (key: string, params?: Record<string, string | number>) => string;
72
+ /**
73
+ * Augment Astro's locals type
74
+ */
75
+ declare global {
76
+ namespace App {
77
+ interface Locals {
78
+ /** Current locale code */
79
+ locale: string;
80
+ /** Loaded translations for the current locale */
81
+ translations: Record<string, unknown>;
82
+ }
83
+ }
84
+ }
85
+
86
+ export type { EzI18nConfig as E, LocaleTranslationPath as L, TranslateFunction as T, TranslationsConfig as a, TranslationCache as b };