@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/README.md +125 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +516 -24
- package/dist/runtime/react-plugin.d.ts +1 -1
- package/dist/runtime/vue-plugin.d.ts +1 -1
- package/dist/types-Cd9e7Lkc.d.ts +98 -0
- package/dist/utils/index.d.ts +85 -0
- package/dist/utils/index.js +220 -0
- package/package.json +8 -1
- package/src/types.ts +74 -12
- package/src/utils/index.ts +16 -0
- package/src/utils/translations.ts +378 -0
- package/src/vite-plugin.ts +471 -29
- package/dist/types-DwCG8sp8.d.ts +0 -48
package/src/vite-plugin.ts
CHANGED
|
@@ -1,33 +1,191 @@
|
|
|
1
|
-
import type { Plugin } from 'vite';
|
|
1
|
+
import type { Plugin, ResolvedConfig, ViteDevServer } from 'vite';
|
|
2
2
|
import type { EzI18nConfig, ResolvedEzI18nConfig } from './types';
|
|
3
|
+
import {
|
|
4
|
+
resolveTranslationsConfig,
|
|
5
|
+
toRelativeImport,
|
|
6
|
+
toGlobPattern,
|
|
7
|
+
loadCache,
|
|
8
|
+
saveCache,
|
|
9
|
+
isCacheValid,
|
|
10
|
+
detectPathType,
|
|
11
|
+
getNamespaceFromPath,
|
|
12
|
+
generateNamespaceWrapperCode,
|
|
13
|
+
} from './utils/translations';
|
|
14
|
+
import * as path from 'node:path';
|
|
3
15
|
|
|
4
16
|
const VIRTUAL_CONFIG = 'ez-i18n:config';
|
|
5
17
|
const VIRTUAL_RUNTIME = 'ez-i18n:runtime';
|
|
6
18
|
const VIRTUAL_TRANSLATIONS = 'ez-i18n:translations';
|
|
7
19
|
const RESOLVED_PREFIX = '\0';
|
|
8
20
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
cookieName: config.cookieName ?? 'ez-locale',
|
|
17
|
-
translations: config.translations ?? {},
|
|
18
|
-
};
|
|
21
|
+
interface TranslationInfo {
|
|
22
|
+
locale: string;
|
|
23
|
+
files: string[];
|
|
24
|
+
/** Glob pattern for dev mode HMR (if applicable) */
|
|
25
|
+
globPattern?: string;
|
|
26
|
+
/** Base directory for this locale (used for namespace calculation) */
|
|
27
|
+
localeBaseDir?: string;
|
|
19
28
|
}
|
|
20
29
|
|
|
21
30
|
/**
|
|
22
31
|
* Vite plugin that provides virtual modules for ez-i18n
|
|
23
32
|
*/
|
|
24
33
|
export function vitePlugin(config: EzI18nConfig): Plugin {
|
|
25
|
-
|
|
34
|
+
let viteConfig: ResolvedConfig;
|
|
35
|
+
let isDev = false;
|
|
36
|
+
let resolved: ResolvedEzI18nConfig;
|
|
37
|
+
let translationInfo: Map<string, TranslationInfo> = new Map();
|
|
26
38
|
|
|
27
39
|
return {
|
|
28
40
|
name: 'ez-i18n-vite',
|
|
29
41
|
enforce: 'pre',
|
|
30
42
|
|
|
43
|
+
configResolved(resolvedConfig) {
|
|
44
|
+
viteConfig = resolvedConfig;
|
|
45
|
+
isDev = resolvedConfig.command === 'serve';
|
|
46
|
+
},
|
|
47
|
+
|
|
48
|
+
async buildStart() {
|
|
49
|
+
const projectRoot = viteConfig.root;
|
|
50
|
+
|
|
51
|
+
// Determine if path-based namespacing should be enabled
|
|
52
|
+
// Default to true when using folder-based or auto-discovery config
|
|
53
|
+
const isAutoDiscovery = !config.translations || typeof config.translations === 'string';
|
|
54
|
+
const pathBasedNamespacing = config.pathBasedNamespacing ?? isAutoDiscovery;
|
|
55
|
+
|
|
56
|
+
// Calculate base translation directory
|
|
57
|
+
const translationsBaseDir = typeof config.translations === 'string'
|
|
58
|
+
? path.resolve(projectRoot, config.translations.replace(/\/$/, ''))
|
|
59
|
+
: path.resolve(projectRoot, './public/i18n');
|
|
60
|
+
|
|
61
|
+
// Try to use cache in production builds
|
|
62
|
+
let useCache = false;
|
|
63
|
+
if (!isDev) {
|
|
64
|
+
const cache = loadCache(projectRoot);
|
|
65
|
+
if (cache && isCacheValid(cache, projectRoot)) {
|
|
66
|
+
// Build locale base dirs
|
|
67
|
+
const localeBaseDirs: Record<string, string> = {};
|
|
68
|
+
for (const locale of Object.keys(cache.discovered)) {
|
|
69
|
+
localeBaseDirs[locale] = path.join(translationsBaseDir, locale);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Use cached discovery
|
|
73
|
+
resolved = {
|
|
74
|
+
locales: config.locales || Object.keys(cache.discovered),
|
|
75
|
+
defaultLocale: config.defaultLocale,
|
|
76
|
+
cookieName: config.cookieName ?? 'ez-locale',
|
|
77
|
+
translations: cache.discovered,
|
|
78
|
+
pathBasedNamespacing,
|
|
79
|
+
localeBaseDirs,
|
|
80
|
+
};
|
|
81
|
+
useCache = true;
|
|
82
|
+
|
|
83
|
+
// Populate translationInfo from cache
|
|
84
|
+
for (const [locale, files] of Object.entries(cache.discovered)) {
|
|
85
|
+
translationInfo.set(locale, {
|
|
86
|
+
locale,
|
|
87
|
+
files,
|
|
88
|
+
localeBaseDir: localeBaseDirs[locale],
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (!useCache) {
|
|
95
|
+
// Resolve translations config
|
|
96
|
+
const { locales, translations } = await resolveTranslationsConfig(
|
|
97
|
+
config.translations,
|
|
98
|
+
projectRoot,
|
|
99
|
+
config.locales
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
// Merge with configured locales (config takes precedence if specified)
|
|
103
|
+
const finalLocales = config.locales && config.locales.length > 0
|
|
104
|
+
? config.locales
|
|
105
|
+
: locales;
|
|
106
|
+
|
|
107
|
+
// Build locale base dirs
|
|
108
|
+
const localeBaseDirs: Record<string, string> = {};
|
|
109
|
+
for (const locale of finalLocales) {
|
|
110
|
+
if (isAutoDiscovery) {
|
|
111
|
+
// For auto-discovery, locale folder is under the base dir
|
|
112
|
+
localeBaseDirs[locale] = path.join(translationsBaseDir, locale);
|
|
113
|
+
} else if (typeof config.translations === 'object' && config.translations[locale]) {
|
|
114
|
+
// For explicit config, determine base from the config value
|
|
115
|
+
const localeConfig = config.translations[locale];
|
|
116
|
+
if (typeof localeConfig === 'string') {
|
|
117
|
+
const pathType = detectPathType(localeConfig);
|
|
118
|
+
if (pathType === 'folder' || pathType === 'file') {
|
|
119
|
+
// Use the folder itself or parent of file
|
|
120
|
+
const resolved = path.resolve(projectRoot, localeConfig.replace(/\/$/, ''));
|
|
121
|
+
localeBaseDirs[locale] = pathType === 'folder' ? resolved : path.dirname(resolved);
|
|
122
|
+
} else {
|
|
123
|
+
// Glob - use the non-glob prefix
|
|
124
|
+
const baseDir = localeConfig.split('*')[0].replace(/\/$/, '');
|
|
125
|
+
localeBaseDirs[locale] = path.resolve(projectRoot, baseDir);
|
|
126
|
+
}
|
|
127
|
+
} else {
|
|
128
|
+
// Array - use common parent directory
|
|
129
|
+
localeBaseDirs[locale] = translationsBaseDir;
|
|
130
|
+
}
|
|
131
|
+
} else {
|
|
132
|
+
localeBaseDirs[locale] = translationsBaseDir;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
resolved = {
|
|
137
|
+
locales: finalLocales,
|
|
138
|
+
defaultLocale: config.defaultLocale,
|
|
139
|
+
cookieName: config.cookieName ?? 'ez-locale',
|
|
140
|
+
translations,
|
|
141
|
+
pathBasedNamespacing,
|
|
142
|
+
localeBaseDirs,
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
// Build translation info for each locale
|
|
146
|
+
for (const locale of finalLocales) {
|
|
147
|
+
const files = translations[locale] || [];
|
|
148
|
+
const info: TranslationInfo = {
|
|
149
|
+
locale,
|
|
150
|
+
files,
|
|
151
|
+
localeBaseDir: localeBaseDirs[locale],
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
// For dev mode, determine if we can use import.meta.glob
|
|
155
|
+
if (isDev && config.translations) {
|
|
156
|
+
const localeConfig = typeof config.translations === 'string'
|
|
157
|
+
? path.join(config.translations, locale)
|
|
158
|
+
: config.translations[locale];
|
|
159
|
+
|
|
160
|
+
if (localeConfig && typeof localeConfig === 'string') {
|
|
161
|
+
const pathType = detectPathType(localeConfig);
|
|
162
|
+
if (pathType === 'folder' || pathType === 'glob') {
|
|
163
|
+
// Can use import.meta.glob for HMR
|
|
164
|
+
const basePath = pathType === 'glob'
|
|
165
|
+
? localeConfig
|
|
166
|
+
: toGlobPattern(path.resolve(projectRoot, localeConfig), projectRoot);
|
|
167
|
+
info.globPattern = basePath;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
translationInfo.set(locale, info);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Save cache for future builds
|
|
176
|
+
if (!isDev && Object.keys(translations).length > 0) {
|
|
177
|
+
saveCache(projectRoot, translations);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Validate defaultLocale
|
|
182
|
+
if (!resolved.locales.includes(resolved.defaultLocale)) {
|
|
183
|
+
console.warn(
|
|
184
|
+
`[ez-i18n] defaultLocale "${resolved.defaultLocale}" not found in locales: [${resolved.locales.join(', ')}]`
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
},
|
|
188
|
+
|
|
31
189
|
resolveId(id) {
|
|
32
190
|
if (id === VIRTUAL_CONFIG || id === VIRTUAL_RUNTIME || id === VIRTUAL_TRANSLATIONS) {
|
|
33
191
|
return RESOLVED_PREFIX + id;
|
|
@@ -99,25 +257,187 @@ export function t(key, params) {
|
|
|
99
257
|
|
|
100
258
|
// ez-i18n:translations - Translation loaders
|
|
101
259
|
if (id === RESOLVED_PREFIX + VIRTUAL_TRANSLATIONS) {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
260
|
+
return isDev
|
|
261
|
+
? generateDevTranslationsModule(translationInfo, viteConfig.root, resolved.pathBasedNamespacing)
|
|
262
|
+
: generateBuildTranslationsModule(translationInfo, viteConfig.root, resolved.pathBasedNamespacing);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
return null;
|
|
266
|
+
},
|
|
267
|
+
|
|
268
|
+
// HMR support for dev mode
|
|
269
|
+
handleHotUpdate({ file, server }) {
|
|
270
|
+
if (!isDev) return;
|
|
271
|
+
|
|
272
|
+
// Check if the changed file is a translation file
|
|
273
|
+
for (const info of translationInfo.values()) {
|
|
274
|
+
if (info.files.includes(file)) {
|
|
275
|
+
// Invalidate the virtual translations module
|
|
276
|
+
const mod = server.moduleGraph.getModuleById(RESOLVED_PREFIX + VIRTUAL_TRANSLATIONS);
|
|
277
|
+
if (mod) {
|
|
278
|
+
server.moduleGraph.invalidateModule(mod);
|
|
279
|
+
// Full reload to ensure translations are updated everywhere
|
|
280
|
+
server.ws.send({
|
|
281
|
+
type: 'full-reload',
|
|
282
|
+
path: '*',
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
break;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
},
|
|
289
|
+
|
|
290
|
+
configureServer(server: ViteDevServer) {
|
|
291
|
+
// Watch translation directories for new/deleted files
|
|
292
|
+
const watchedDirs = new Set<string>();
|
|
293
|
+
|
|
294
|
+
if (config.translations) {
|
|
295
|
+
if (typeof config.translations === 'string') {
|
|
296
|
+
// Base directory
|
|
297
|
+
watchedDirs.add(path.resolve(viteConfig.root, config.translations));
|
|
298
|
+
} else {
|
|
299
|
+
// Per-locale config
|
|
300
|
+
for (const localePath of Object.values(config.translations)) {
|
|
301
|
+
if (typeof localePath === 'string') {
|
|
302
|
+
const pathType = detectPathType(localePath);
|
|
303
|
+
if (pathType === 'folder') {
|
|
304
|
+
watchedDirs.add(path.resolve(viteConfig.root, localePath));
|
|
305
|
+
} else if (pathType === 'glob') {
|
|
306
|
+
// Extract base directory from glob
|
|
307
|
+
const baseDir = localePath.split('*')[0].replace(/\/$/, '');
|
|
308
|
+
if (baseDir) {
|
|
309
|
+
watchedDirs.add(path.resolve(viteConfig.root, baseDir));
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
} else if (Array.isArray(localePath)) {
|
|
313
|
+
// Array of files - watch parent directories
|
|
314
|
+
for (const file of localePath) {
|
|
315
|
+
const dir = path.dirname(path.resolve(viteConfig.root, file));
|
|
316
|
+
watchedDirs.add(dir);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
} else {
|
|
322
|
+
// Default directory
|
|
323
|
+
watchedDirs.add(path.resolve(viteConfig.root, './public/i18n'));
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Add directories to watcher
|
|
327
|
+
for (const dir of watchedDirs) {
|
|
328
|
+
server.watcher.add(dir);
|
|
329
|
+
}
|
|
330
|
+
},
|
|
331
|
+
};
|
|
332
|
+
}
|
|
106
333
|
|
|
107
|
-
return `
|
|
108
334
|
/**
|
|
109
|
-
*
|
|
110
|
-
*
|
|
335
|
+
* Generate the translations virtual module for dev mode.
|
|
336
|
+
* Uses import.meta.glob where possible for HMR support.
|
|
111
337
|
*/
|
|
338
|
+
function generateDevTranslationsModule(
|
|
339
|
+
translationInfo: Map<string, TranslationInfo>,
|
|
340
|
+
projectRoot: string,
|
|
341
|
+
pathBasedNamespacing: boolean
|
|
342
|
+
): string {
|
|
343
|
+
const imports: string[] = [];
|
|
344
|
+
const loaderEntries: string[] = [];
|
|
345
|
+
|
|
346
|
+
// Add deepMerge inline for runtime merging
|
|
347
|
+
imports.push(getDeepMergeCode());
|
|
348
|
+
|
|
349
|
+
// Add namespace wrapper if needed
|
|
350
|
+
if (pathBasedNamespacing) {
|
|
351
|
+
imports.push(generateNamespaceWrapperCode());
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
for (const [locale, info] of translationInfo) {
|
|
355
|
+
if (info.files.length === 0) {
|
|
356
|
+
// No files - return empty object
|
|
357
|
+
loaderEntries.push(` ${JSON.stringify(locale)}: async () => ({})`);
|
|
358
|
+
} else if (info.globPattern && pathBasedNamespacing && info.localeBaseDir) {
|
|
359
|
+
// Use import.meta.glob with namespace wrapping
|
|
360
|
+
const varName = `__${locale}Modules`;
|
|
361
|
+
const localeBaseDir = info.localeBaseDir.replace(/\\/g, '/');
|
|
362
|
+
imports.push(
|
|
363
|
+
`const ${varName} = import.meta.glob(${JSON.stringify(info.globPattern)}, { eager: true, import: 'default' });`
|
|
364
|
+
);
|
|
365
|
+
|
|
366
|
+
loaderEntries.push(` ${JSON.stringify(locale)}: async () => {
|
|
367
|
+
const entries = Object.entries(${varName});
|
|
368
|
+
if (entries.length === 0) return {};
|
|
369
|
+
const localeBaseDir = ${JSON.stringify(localeBaseDir)};
|
|
370
|
+
const wrapped = entries.map(([filePath, content]) => {
|
|
371
|
+
// Extract relative path from locale base dir
|
|
372
|
+
const relativePath = filePath.replace(localeBaseDir + '/', '').replace(/\\.json$/i, '');
|
|
373
|
+
const namespace = relativePath.replace(/[\\/]/g, '.').replace(/\\.index$/, '');
|
|
374
|
+
return __wrapWithNamespace(namespace, content);
|
|
375
|
+
});
|
|
376
|
+
return __deepMerge({}, ...wrapped);
|
|
377
|
+
}`);
|
|
378
|
+
} else if (info.globPattern) {
|
|
379
|
+
// Use import.meta.glob without namespace wrapping
|
|
380
|
+
const varName = `__${locale}Modules`;
|
|
381
|
+
imports.push(
|
|
382
|
+
`const ${varName} = import.meta.glob(${JSON.stringify(info.globPattern)}, { eager: true, import: 'default' });`
|
|
383
|
+
);
|
|
384
|
+
|
|
385
|
+
loaderEntries.push(` ${JSON.stringify(locale)}: async () => {
|
|
386
|
+
const modules = Object.values(${varName});
|
|
387
|
+
if (modules.length === 0) return {};
|
|
388
|
+
if (modules.length === 1) return modules[0];
|
|
389
|
+
return __deepMerge({}, ...modules);
|
|
390
|
+
}`);
|
|
391
|
+
} else if (info.files.length === 1) {
|
|
392
|
+
// Single file
|
|
393
|
+
const relativePath = toRelativeImport(info.files[0], projectRoot);
|
|
394
|
+
if (pathBasedNamespacing && info.localeBaseDir) {
|
|
395
|
+
const namespace = getNamespaceFromPath(info.files[0], info.localeBaseDir);
|
|
396
|
+
loaderEntries.push(
|
|
397
|
+
` ${JSON.stringify(locale)}: () => import(${JSON.stringify(relativePath)}).then(m => __wrapWithNamespace(${JSON.stringify(namespace)}, m.default ?? m))`
|
|
398
|
+
);
|
|
399
|
+
} else {
|
|
400
|
+
loaderEntries.push(
|
|
401
|
+
` ${JSON.stringify(locale)}: () => import(${JSON.stringify(relativePath)}).then(m => m.default ?? m)`
|
|
402
|
+
);
|
|
403
|
+
}
|
|
404
|
+
} else {
|
|
405
|
+
// Multiple explicit files - import all and merge
|
|
406
|
+
if (pathBasedNamespacing && info.localeBaseDir) {
|
|
407
|
+
const fileEntries = info.files.map(f => {
|
|
408
|
+
const relativePath = toRelativeImport(f, projectRoot);
|
|
409
|
+
const namespace = getNamespaceFromPath(f, info.localeBaseDir!);
|
|
410
|
+
return `{ path: ${JSON.stringify(relativePath)}, namespace: ${JSON.stringify(namespace)} }`;
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
loaderEntries.push(` ${JSON.stringify(locale)}: async () => {
|
|
414
|
+
const fileInfos = [${fileEntries.join(', ')}];
|
|
415
|
+
const modules = await Promise.all(fileInfos.map(f => import(f.path)));
|
|
416
|
+
const wrapped = modules.map((m, i) => __wrapWithNamespace(fileInfos[i].namespace, m.default ?? m));
|
|
417
|
+
return __deepMerge({}, ...wrapped);
|
|
418
|
+
}`);
|
|
419
|
+
} else {
|
|
420
|
+
const fileImports = info.files
|
|
421
|
+
.map(f => `import(${JSON.stringify(toRelativeImport(f, projectRoot))})`)
|
|
422
|
+
.join(', ');
|
|
423
|
+
|
|
424
|
+
loaderEntries.push(` ${JSON.stringify(locale)}: async () => {
|
|
425
|
+
const modules = await Promise.all([${fileImports}]);
|
|
426
|
+
const contents = modules.map(m => m.default ?? m);
|
|
427
|
+
if (contents.length === 1) return contents[0];
|
|
428
|
+
return __deepMerge({}, ...contents);
|
|
429
|
+
}`);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
return `
|
|
435
|
+
${imports.join('\n')}
|
|
436
|
+
|
|
112
437
|
export const translationLoaders = {
|
|
113
|
-
${loaderEntries}
|
|
438
|
+
${loaderEntries.join(',\n')}
|
|
114
439
|
};
|
|
115
440
|
|
|
116
|
-
/**
|
|
117
|
-
* Load translations for a specific locale
|
|
118
|
-
* @param locale - Locale code to load translations for
|
|
119
|
-
* @returns Translations object or empty object if not found
|
|
120
|
-
*/
|
|
121
441
|
export async function loadTranslations(locale) {
|
|
122
442
|
const loader = translationLoaders[locale];
|
|
123
443
|
if (!loader) {
|
|
@@ -128,8 +448,7 @@ export async function loadTranslations(locale) {
|
|
|
128
448
|
}
|
|
129
449
|
|
|
130
450
|
try {
|
|
131
|
-
|
|
132
|
-
return mod.default ?? mod;
|
|
451
|
+
return await loader();
|
|
133
452
|
} catch (error) {
|
|
134
453
|
if (import.meta.env.DEV) {
|
|
135
454
|
console.error('[ez-i18n] Failed to load translations for locale:', locale, error);
|
|
@@ -138,9 +457,132 @@ export async function loadTranslations(locale) {
|
|
|
138
457
|
}
|
|
139
458
|
}
|
|
140
459
|
`;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* Generate the translations virtual module for production builds.
|
|
464
|
+
* Pre-resolves all imports for optimal bundling.
|
|
465
|
+
*/
|
|
466
|
+
function generateBuildTranslationsModule(
|
|
467
|
+
translationInfo: Map<string, TranslationInfo>,
|
|
468
|
+
projectRoot: string,
|
|
469
|
+
pathBasedNamespacing: boolean
|
|
470
|
+
): string {
|
|
471
|
+
const loaderEntries: string[] = [];
|
|
472
|
+
let needsDeepMerge = false;
|
|
473
|
+
let needsNamespaceWrapper = false;
|
|
474
|
+
|
|
475
|
+
for (const [locale, info] of translationInfo) {
|
|
476
|
+
if (info.files.length === 0) {
|
|
477
|
+
loaderEntries.push(` ${JSON.stringify(locale)}: async () => ({})`);
|
|
478
|
+
} else if (info.files.length === 1) {
|
|
479
|
+
// Single file
|
|
480
|
+
const relativePath = toRelativeImport(info.files[0], projectRoot);
|
|
481
|
+
if (pathBasedNamespacing && info.localeBaseDir) {
|
|
482
|
+
needsNamespaceWrapper = true;
|
|
483
|
+
const namespace = getNamespaceFromPath(info.files[0], info.localeBaseDir);
|
|
484
|
+
loaderEntries.push(
|
|
485
|
+
` ${JSON.stringify(locale)}: () => import(${JSON.stringify(relativePath)}).then(m => __wrapWithNamespace(${JSON.stringify(namespace)}, m.default ?? m))`
|
|
486
|
+
);
|
|
487
|
+
} else {
|
|
488
|
+
loaderEntries.push(
|
|
489
|
+
` ${JSON.stringify(locale)}: () => import(${JSON.stringify(relativePath)}).then(m => m.default ?? m)`
|
|
490
|
+
);
|
|
141
491
|
}
|
|
492
|
+
} else {
|
|
493
|
+
// Multiple files - import and merge
|
|
494
|
+
needsDeepMerge = true;
|
|
142
495
|
|
|
143
|
-
|
|
144
|
-
|
|
496
|
+
if (pathBasedNamespacing && info.localeBaseDir) {
|
|
497
|
+
needsNamespaceWrapper = true;
|
|
498
|
+
const fileEntries = info.files.map(f => {
|
|
499
|
+
const relativePath = toRelativeImport(f, projectRoot);
|
|
500
|
+
const namespace = getNamespaceFromPath(f, info.localeBaseDir!);
|
|
501
|
+
return `{ path: ${JSON.stringify(relativePath)}, namespace: ${JSON.stringify(namespace)} }`;
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
loaderEntries.push(` ${JSON.stringify(locale)}: async () => {
|
|
505
|
+
const fileInfos = [${fileEntries.join(', ')}];
|
|
506
|
+
const modules = await Promise.all(fileInfos.map(f => import(f.path)));
|
|
507
|
+
const wrapped = modules.map((m, i) => __wrapWithNamespace(fileInfos[i].namespace, m.default ?? m));
|
|
508
|
+
return __deepMerge({}, ...wrapped);
|
|
509
|
+
}`);
|
|
510
|
+
} else {
|
|
511
|
+
const fileImports = info.files
|
|
512
|
+
.map(f => `import(${JSON.stringify(toRelativeImport(f, projectRoot))})`)
|
|
513
|
+
.join(', ');
|
|
514
|
+
|
|
515
|
+
loaderEntries.push(` ${JSON.stringify(locale)}: async () => {
|
|
516
|
+
const modules = await Promise.all([${fileImports}]);
|
|
517
|
+
const contents = modules.map(m => m.default ?? m);
|
|
518
|
+
return __deepMerge({}, ...contents);
|
|
519
|
+
}`);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
const helperCode = [
|
|
525
|
+
needsDeepMerge ? getDeepMergeCode() : '',
|
|
526
|
+
needsNamespaceWrapper ? generateNamespaceWrapperCode() : '',
|
|
527
|
+
].filter(Boolean).join('\n');
|
|
528
|
+
|
|
529
|
+
return `
|
|
530
|
+
${helperCode}
|
|
531
|
+
|
|
532
|
+
export const translationLoaders = {
|
|
533
|
+
${loaderEntries.join(',\n')}
|
|
534
|
+
};
|
|
535
|
+
|
|
536
|
+
export async function loadTranslations(locale) {
|
|
537
|
+
const loader = translationLoaders[locale];
|
|
538
|
+
if (!loader) {
|
|
539
|
+
return {};
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
try {
|
|
543
|
+
return await loader();
|
|
544
|
+
} catch (error) {
|
|
545
|
+
console.error('[ez-i18n] Failed to load translations:', locale, error);
|
|
546
|
+
return {};
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
`;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
/**
|
|
553
|
+
* Inline deepMerge function for the virtual module
|
|
554
|
+
*/
|
|
555
|
+
function getDeepMergeCode(): string {
|
|
556
|
+
return `
|
|
557
|
+
function __deepMerge(target, ...sources) {
|
|
558
|
+
const FORBIDDEN = new Set(['__proto__', 'constructor', 'prototype']);
|
|
559
|
+
const result = { ...target };
|
|
560
|
+
for (const source of sources) {
|
|
561
|
+
if (!source || typeof source !== 'object') continue;
|
|
562
|
+
for (const key of Object.keys(source)) {
|
|
563
|
+
if (FORBIDDEN.has(key)) continue;
|
|
564
|
+
const tv = result[key], sv = source[key];
|
|
565
|
+
if (sv && typeof sv === 'object' && !Array.isArray(sv) && tv && typeof tv === 'object' && !Array.isArray(tv)) {
|
|
566
|
+
result[key] = __deepMerge(tv, sv);
|
|
567
|
+
} else {
|
|
568
|
+
result[key] = sv;
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
return result;
|
|
573
|
+
}`;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// Re-export resolveConfig for backwards compatibility
|
|
577
|
+
export function resolveConfig(config: EzI18nConfig): ResolvedEzI18nConfig {
|
|
578
|
+
// This is now a simplified version - full resolution happens in buildStart
|
|
579
|
+
const isAutoDiscovery = !config.translations || typeof config.translations === 'string';
|
|
580
|
+
return {
|
|
581
|
+
locales: config.locales || [],
|
|
582
|
+
defaultLocale: config.defaultLocale,
|
|
583
|
+
cookieName: config.cookieName ?? 'ez-locale',
|
|
584
|
+
translations: {},
|
|
585
|
+
pathBasedNamespacing: config.pathBasedNamespacing ?? isAutoDiscovery,
|
|
586
|
+
localeBaseDirs: {},
|
|
145
587
|
};
|
|
146
588
|
}
|
package/dist/types-DwCG8sp8.d.ts
DELETED
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Configuration for ez-i18n Astro integration
|
|
3
|
-
*/
|
|
4
|
-
interface EzI18nConfig {
|
|
5
|
-
/**
|
|
6
|
-
* List of supported locale codes (e.g., ['en', 'es', 'fr'])
|
|
7
|
-
*/
|
|
8
|
-
locales: string[];
|
|
9
|
-
/**
|
|
10
|
-
* Default locale to use when no preference is detected
|
|
11
|
-
*/
|
|
12
|
-
defaultLocale: string;
|
|
13
|
-
/**
|
|
14
|
-
* Cookie name for storing locale preference
|
|
15
|
-
* @default 'ez-locale'
|
|
16
|
-
*/
|
|
17
|
-
cookieName?: string;
|
|
18
|
-
/**
|
|
19
|
-
* Translation file paths for each locale.
|
|
20
|
-
* Paths are relative to your project root.
|
|
21
|
-
* Dynamic imports will be generated for code splitting.
|
|
22
|
-
* @example
|
|
23
|
-
* translations: {
|
|
24
|
-
* en: './src/i18n/en.json',
|
|
25
|
-
* es: './src/i18n/es.json',
|
|
26
|
-
* }
|
|
27
|
-
*/
|
|
28
|
-
translations?: Record<string, string>;
|
|
29
|
-
}
|
|
30
|
-
/**
|
|
31
|
-
* Translation function type
|
|
32
|
-
*/
|
|
33
|
-
type TranslateFunction = (key: string, params?: Record<string, string | number>) => string;
|
|
34
|
-
/**
|
|
35
|
-
* Augment Astro's locals type
|
|
36
|
-
*/
|
|
37
|
-
declare global {
|
|
38
|
-
namespace App {
|
|
39
|
-
interface Locals {
|
|
40
|
-
/** Current locale code */
|
|
41
|
-
locale: string;
|
|
42
|
-
/** Loaded translations for the current locale */
|
|
43
|
-
translations: Record<string, unknown>;
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
export type { EzI18nConfig as E, TranslateFunction as T };
|