@zachhandley/ez-i18n 0.1.9 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +115 -7
- package/dist/utils/index.d.ts +2 -1
- package/dist/utils/index.js +3 -0
- package/package.json +1 -1
- package/src/utils/translations.ts +40 -1
- package/src/vite-plugin.ts +118 -11
package/dist/index.js
CHANGED
|
@@ -152,8 +152,29 @@ function toRelativeImport(absolutePath, projectRoot) {
|
|
|
152
152
|
}
|
|
153
153
|
function toGlobPattern(baseDir, projectRoot) {
|
|
154
154
|
const relativePath = path.relative(projectRoot, baseDir).replace(/\\/g, "/");
|
|
155
|
+
if (relativePath.startsWith("public/") || relativePath === "public") {
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
155
158
|
return `/${relativePath}/**/*.json`;
|
|
156
159
|
}
|
|
160
|
+
function isInPublicDir(filePath, projectRoot) {
|
|
161
|
+
const relativePath = path.relative(projectRoot, filePath).replace(/\\/g, "/");
|
|
162
|
+
return relativePath.startsWith("public/") || relativePath === "public";
|
|
163
|
+
}
|
|
164
|
+
function toPublicUrl(filePath, projectRoot) {
|
|
165
|
+
const relativePath = path.relative(projectRoot, filePath).replace(/\\/g, "/");
|
|
166
|
+
if (relativePath.startsWith("public/")) {
|
|
167
|
+
return "/" + relativePath.slice("public/".length);
|
|
168
|
+
}
|
|
169
|
+
return "/" + relativePath;
|
|
170
|
+
}
|
|
171
|
+
function getLocaleBaseDirForNamespace(localeBaseDir, projectRoot) {
|
|
172
|
+
const relativePath = path.relative(projectRoot, localeBaseDir).replace(/\\/g, "/");
|
|
173
|
+
if (relativePath.startsWith("public/")) {
|
|
174
|
+
return relativePath.slice("public/".length);
|
|
175
|
+
}
|
|
176
|
+
return relativePath;
|
|
177
|
+
}
|
|
157
178
|
function getNamespaceFromPath(filePath, localeDir) {
|
|
158
179
|
const relative2 = path.relative(localeDir, filePath);
|
|
159
180
|
const withoutExt = relative2.replace(/\.json$/i, "");
|
|
@@ -261,12 +282,14 @@ function vitePlugin(config) {
|
|
|
261
282
|
};
|
|
262
283
|
for (const locale of finalLocales) {
|
|
263
284
|
const files = translations[locale] || [];
|
|
285
|
+
const filesInPublic = files.length > 0 && isInPublicDir(files[0], projectRoot);
|
|
264
286
|
const info = {
|
|
265
287
|
locale,
|
|
266
288
|
files,
|
|
267
|
-
localeBaseDir: localeBaseDirs[locale]
|
|
289
|
+
localeBaseDir: localeBaseDirs[locale],
|
|
290
|
+
isPublic: filesInPublic
|
|
268
291
|
};
|
|
269
|
-
if (isDev && config.translations) {
|
|
292
|
+
if (isDev && config.translations && !filesInPublic) {
|
|
270
293
|
const localeConfig = typeof config.translations === "string" ? path2.join(config.translations, locale) + "/" : config.translations[locale];
|
|
271
294
|
if (localeConfig && typeof localeConfig === "string") {
|
|
272
295
|
const pathType = detectPathType(localeConfig);
|
|
@@ -412,6 +435,7 @@ export function t(key, params) {
|
|
|
412
435
|
function generateDevTranslationsModule(translationInfo, projectRoot, pathBasedNamespacing) {
|
|
413
436
|
const imports = [];
|
|
414
437
|
const loaderEntries = [];
|
|
438
|
+
let needsPublicLoader = false;
|
|
415
439
|
imports.push(getDeepMergeCode());
|
|
416
440
|
if (pathBasedNamespacing) {
|
|
417
441
|
imports.push(generateNamespaceWrapperCode());
|
|
@@ -419,19 +443,48 @@ function generateDevTranslationsModule(translationInfo, projectRoot, pathBasedNa
|
|
|
419
443
|
for (const [locale, info] of translationInfo) {
|
|
420
444
|
if (info.files.length === 0) {
|
|
421
445
|
loaderEntries.push(` ${JSON.stringify(locale)}: async () => ({})`);
|
|
446
|
+
} else if (info.isPublic) {
|
|
447
|
+
needsPublicLoader = true;
|
|
448
|
+
if (pathBasedNamespacing && info.localeBaseDir) {
|
|
449
|
+
const fileEntries = info.files.map((f) => {
|
|
450
|
+
const url = toPublicUrl(f, projectRoot);
|
|
451
|
+
const absolutePath = f.replace(/\\/g, "/");
|
|
452
|
+
const namespace = getNamespaceFromPath(f, info.localeBaseDir);
|
|
453
|
+
return `{ url: ${JSON.stringify(url)}, path: ${JSON.stringify(absolutePath)}, namespace: ${JSON.stringify(namespace)} }`;
|
|
454
|
+
});
|
|
455
|
+
loaderEntries.push(` ${JSON.stringify(locale)}: async () => {
|
|
456
|
+
const fileInfos = [${fileEntries.join(", ")}];
|
|
457
|
+
const responses = await Promise.all(fileInfos.map(f => __loadPublicJson(f.url, f.path)));
|
|
458
|
+
const wrapped = responses.map((content, i) => __wrapWithNamespace(fileInfos[i].namespace, content));
|
|
459
|
+
return __deepMerge({}, ...wrapped);
|
|
460
|
+
}`);
|
|
461
|
+
} else {
|
|
462
|
+
const fileEntries = info.files.map((f) => {
|
|
463
|
+
const url = toPublicUrl(f, projectRoot);
|
|
464
|
+
const absolutePath = f.replace(/\\/g, "/");
|
|
465
|
+
return `{ url: ${JSON.stringify(url)}, path: ${JSON.stringify(absolutePath)} }`;
|
|
466
|
+
});
|
|
467
|
+
loaderEntries.push(` ${JSON.stringify(locale)}: async () => {
|
|
468
|
+
const files = [${fileEntries.join(", ")}];
|
|
469
|
+
const responses = await Promise.all(files.map(f => __loadPublicJson(f.url, f.path)));
|
|
470
|
+
if (responses.length === 1) return responses[0];
|
|
471
|
+
return __deepMerge({}, ...responses);
|
|
472
|
+
}`);
|
|
473
|
+
}
|
|
422
474
|
} else if (info.globPattern && pathBasedNamespacing && info.localeBaseDir) {
|
|
423
475
|
const varName = `__${locale}Modules`;
|
|
424
|
-
const
|
|
476
|
+
const localeBaseDirForNs = getLocaleBaseDirForNamespace(info.localeBaseDir, projectRoot);
|
|
425
477
|
imports.push(
|
|
426
478
|
`const ${varName} = import.meta.glob(${JSON.stringify(info.globPattern)}, { eager: true, import: 'default' });`
|
|
427
479
|
);
|
|
428
480
|
loaderEntries.push(` ${JSON.stringify(locale)}: async () => {
|
|
429
481
|
const entries = Object.entries(${varName});
|
|
430
482
|
if (entries.length === 0) return {};
|
|
431
|
-
const localeBaseDir = ${JSON.stringify(
|
|
483
|
+
const localeBaseDir = ${JSON.stringify(localeBaseDirForNs)};
|
|
432
484
|
const wrapped = entries.map(([filePath, content]) => {
|
|
433
|
-
// Extract relative path from locale base dir
|
|
434
|
-
const
|
|
485
|
+
// Extract relative path from locale base dir - filePath starts with /
|
|
486
|
+
const normalizedPath = filePath.startsWith('/') ? filePath.slice(1) : filePath;
|
|
487
|
+
const relativePath = normalizedPath.replace(localeBaseDir + '/', '').replace(/\\.json$/i, '');
|
|
435
488
|
const namespace = relativePath.replace(/[\\/]/g, '.').replace(/\\.index$/, '');
|
|
436
489
|
return __wrapWithNamespace(namespace, content);
|
|
437
490
|
});
|
|
@@ -484,6 +537,9 @@ function generateDevTranslationsModule(translationInfo, projectRoot, pathBasedNa
|
|
|
484
537
|
}
|
|
485
538
|
}
|
|
486
539
|
}
|
|
540
|
+
if (needsPublicLoader) {
|
|
541
|
+
imports.push(getPublicLoaderCode());
|
|
542
|
+
}
|
|
487
543
|
return `
|
|
488
544
|
${imports.join("\n")}
|
|
489
545
|
|
|
@@ -515,9 +571,46 @@ function generateBuildTranslationsModule(translationInfo, projectRoot, pathBased
|
|
|
515
571
|
const loaderEntries = [];
|
|
516
572
|
let needsDeepMerge = false;
|
|
517
573
|
let needsNamespaceWrapper = false;
|
|
574
|
+
let needsPublicLoader = false;
|
|
518
575
|
for (const [locale, info] of translationInfo) {
|
|
519
576
|
if (info.files.length === 0) {
|
|
520
577
|
loaderEntries.push(` ${JSON.stringify(locale)}: async () => ({})`);
|
|
578
|
+
} else if (info.isPublic) {
|
|
579
|
+
needsPublicLoader = true;
|
|
580
|
+
needsDeepMerge = info.files.length > 1;
|
|
581
|
+
if (pathBasedNamespacing && info.localeBaseDir) {
|
|
582
|
+
needsNamespaceWrapper = true;
|
|
583
|
+
const fileEntries = info.files.map((f) => {
|
|
584
|
+
const url = toPublicUrl(f, projectRoot);
|
|
585
|
+
const absolutePath = f.replace(/\\/g, "/");
|
|
586
|
+
const namespace = getNamespaceFromPath(f, info.localeBaseDir);
|
|
587
|
+
return `{ url: ${JSON.stringify(url)}, path: ${JSON.stringify(absolutePath)}, namespace: ${JSON.stringify(namespace)} }`;
|
|
588
|
+
});
|
|
589
|
+
loaderEntries.push(` ${JSON.stringify(locale)}: async () => {
|
|
590
|
+
const fileInfos = [${fileEntries.join(", ")}];
|
|
591
|
+
const responses = await Promise.all(fileInfos.map(f => __loadPublicJson(f.url, f.path)));
|
|
592
|
+
const wrapped = responses.map((content, i) => __wrapWithNamespace(fileInfos[i].namespace, content));
|
|
593
|
+
return __deepMerge({}, ...wrapped);
|
|
594
|
+
}`);
|
|
595
|
+
} else {
|
|
596
|
+
const fileEntries = info.files.map((f) => {
|
|
597
|
+
const url = toPublicUrl(f, projectRoot);
|
|
598
|
+
const absolutePath = f.replace(/\\/g, "/");
|
|
599
|
+
return `{ url: ${JSON.stringify(url)}, path: ${JSON.stringify(absolutePath)} }`;
|
|
600
|
+
});
|
|
601
|
+
if (fileEntries.length === 1) {
|
|
602
|
+
const f = info.files[0];
|
|
603
|
+
const url = toPublicUrl(f, projectRoot);
|
|
604
|
+
const absolutePath = f.replace(/\\/g, "/");
|
|
605
|
+
loaderEntries.push(` ${JSON.stringify(locale)}: () => __loadPublicJson(${JSON.stringify(url)}, ${JSON.stringify(absolutePath)})`);
|
|
606
|
+
} else {
|
|
607
|
+
loaderEntries.push(` ${JSON.stringify(locale)}: async () => {
|
|
608
|
+
const files = [${fileEntries.join(", ")}];
|
|
609
|
+
const responses = await Promise.all(files.map(f => __loadPublicJson(f.url, f.path)));
|
|
610
|
+
return __deepMerge({}, ...responses);
|
|
611
|
+
}`);
|
|
612
|
+
}
|
|
613
|
+
}
|
|
521
614
|
} else if (info.files.length === 1) {
|
|
522
615
|
const relativePath = toRelativeImport(info.files[0], projectRoot);
|
|
523
616
|
if (pathBasedNamespacing && info.localeBaseDir) {
|
|
@@ -558,7 +651,8 @@ function generateBuildTranslationsModule(translationInfo, projectRoot, pathBased
|
|
|
558
651
|
}
|
|
559
652
|
const helperCode = [
|
|
560
653
|
needsDeepMerge ? getDeepMergeCode() : "",
|
|
561
|
-
needsNamespaceWrapper ? generateNamespaceWrapperCode() : ""
|
|
654
|
+
needsNamespaceWrapper ? generateNamespaceWrapperCode() : "",
|
|
655
|
+
needsPublicLoader ? getPublicLoaderCode() : ""
|
|
562
656
|
].filter(Boolean).join("\n");
|
|
563
657
|
return `
|
|
564
658
|
${helperCode}
|
|
@@ -602,6 +696,20 @@ function __deepMerge(target, ...sources) {
|
|
|
602
696
|
return result;
|
|
603
697
|
}`;
|
|
604
698
|
}
|
|
699
|
+
function getPublicLoaderCode() {
|
|
700
|
+
return `
|
|
701
|
+
async function __loadPublicJson(url, absolutePath) {
|
|
702
|
+
if (typeof window !== 'undefined') {
|
|
703
|
+
// Browser - use fetch with relative URL
|
|
704
|
+
return fetch(url).then(r => r.json());
|
|
705
|
+
} else {
|
|
706
|
+
// SSR/Node - read from filesystem
|
|
707
|
+
const fs = await import('node:fs');
|
|
708
|
+
const content = fs.readFileSync(absolutePath, 'utf-8');
|
|
709
|
+
return JSON.parse(content);
|
|
710
|
+
}
|
|
711
|
+
}`;
|
|
712
|
+
}
|
|
605
713
|
function resolveConfig(config) {
|
|
606
714
|
const isAutoDiscovery = !config.translations || typeof config.translations === "string";
|
|
607
715
|
return {
|
package/dist/utils/index.d.ts
CHANGED
|
@@ -54,8 +54,9 @@ declare function toRelativeImport(absolutePath: string, projectRoot: string): st
|
|
|
54
54
|
/**
|
|
55
55
|
* Generate a glob pattern for import.meta.glob from a base directory.
|
|
56
56
|
* In virtual modules, globs must start with '/' (project root relative).
|
|
57
|
+
* Returns null if the path is in public/ (can't use import.meta.glob for public files).
|
|
57
58
|
*/
|
|
58
|
-
declare function toGlobPattern(baseDir: string, projectRoot: string): string;
|
|
59
|
+
declare function toGlobPattern(baseDir: string, projectRoot: string): string | null;
|
|
59
60
|
/**
|
|
60
61
|
* Get namespace from file path relative to locale base directory.
|
|
61
62
|
*
|
package/dist/utils/index.js
CHANGED
|
@@ -173,6 +173,9 @@ function toRelativeImport(absolutePath, projectRoot) {
|
|
|
173
173
|
}
|
|
174
174
|
function toGlobPattern(baseDir, projectRoot) {
|
|
175
175
|
const relativePath = path.relative(projectRoot, baseDir).replace(/\\/g, "/");
|
|
176
|
+
if (relativePath.startsWith("public/") || relativePath === "public") {
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
176
179
|
return `/${relativePath}/**/*.json`;
|
|
177
180
|
}
|
|
178
181
|
function getNamespaceFromPath(filePath, localeDir) {
|
package/package.json
CHANGED
|
@@ -304,13 +304,52 @@ export function toRelativeImport(absolutePath: string, projectRoot: string): str
|
|
|
304
304
|
/**
|
|
305
305
|
* Generate a glob pattern for import.meta.glob from a base directory.
|
|
306
306
|
* In virtual modules, globs must start with '/' (project root relative).
|
|
307
|
+
* Returns null if the path is in public/ (can't use import.meta.glob for public files).
|
|
307
308
|
*/
|
|
308
|
-
export function toGlobPattern(baseDir: string, projectRoot: string): string {
|
|
309
|
+
export function toGlobPattern(baseDir: string, projectRoot: string): string | null {
|
|
309
310
|
const relativePath = path.relative(projectRoot, baseDir).replace(/\\/g, '/');
|
|
311
|
+
// Can't use import.meta.glob for public directory files
|
|
312
|
+
if (relativePath.startsWith('public/') || relativePath === 'public') {
|
|
313
|
+
return null;
|
|
314
|
+
}
|
|
310
315
|
// Virtual modules require globs to start with '/' (project root relative)
|
|
311
316
|
return `/${relativePath}/**/*.json`;
|
|
312
317
|
}
|
|
313
318
|
|
|
319
|
+
/**
|
|
320
|
+
* Check if an absolute path is inside the public directory
|
|
321
|
+
*/
|
|
322
|
+
export function isInPublicDir(filePath: string, projectRoot: string): boolean {
|
|
323
|
+
const relativePath = path.relative(projectRoot, filePath).replace(/\\/g, '/');
|
|
324
|
+
return relativePath.startsWith('public/') || relativePath === 'public';
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Convert a public directory path to its served URL.
|
|
329
|
+
* public/i18n/en/common.json → /i18n/en/common.json
|
|
330
|
+
*/
|
|
331
|
+
export function toPublicUrl(filePath: string, projectRoot: string): string {
|
|
332
|
+
const relativePath = path.relative(projectRoot, filePath).replace(/\\/g, '/');
|
|
333
|
+
// Remove 'public/' prefix - files in public/ are served at root
|
|
334
|
+
if (relativePath.startsWith('public/')) {
|
|
335
|
+
return '/' + relativePath.slice('public/'.length);
|
|
336
|
+
}
|
|
337
|
+
return '/' + relativePath;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Get the locale base directory for namespace calculation.
|
|
342
|
+
* For public paths, strips the 'public/' prefix.
|
|
343
|
+
*/
|
|
344
|
+
export function getLocaleBaseDirForNamespace(localeBaseDir: string, projectRoot: string): string {
|
|
345
|
+
const relativePath = path.relative(projectRoot, localeBaseDir).replace(/\\/g, '/');
|
|
346
|
+
// For namespace calculation, we want the path without 'public/' prefix
|
|
347
|
+
if (relativePath.startsWith('public/')) {
|
|
348
|
+
return relativePath.slice('public/'.length);
|
|
349
|
+
}
|
|
350
|
+
return relativePath;
|
|
351
|
+
}
|
|
352
|
+
|
|
314
353
|
/**
|
|
315
354
|
* Get namespace from file path relative to locale base directory.
|
|
316
355
|
*
|
package/src/vite-plugin.ts
CHANGED
|
@@ -10,6 +10,9 @@ import {
|
|
|
10
10
|
detectPathType,
|
|
11
11
|
getNamespaceFromPath,
|
|
12
12
|
generateNamespaceWrapperCode,
|
|
13
|
+
isInPublicDir,
|
|
14
|
+
toPublicUrl,
|
|
15
|
+
getLocaleBaseDirForNamespace,
|
|
13
16
|
} from './utils/translations';
|
|
14
17
|
import * as path from 'node:path';
|
|
15
18
|
|
|
@@ -21,10 +24,12 @@ const RESOLVED_PREFIX = '\0';
|
|
|
21
24
|
interface TranslationInfo {
|
|
22
25
|
locale: string;
|
|
23
26
|
files: string[];
|
|
24
|
-
/** Glob pattern for dev mode HMR (if applicable) */
|
|
25
|
-
globPattern?: string;
|
|
27
|
+
/** Glob pattern for dev mode HMR (if applicable, null for public files) */
|
|
28
|
+
globPattern?: string | null;
|
|
26
29
|
/** Base directory for this locale (used for namespace calculation) */
|
|
27
30
|
localeBaseDir?: string;
|
|
31
|
+
/** Whether files are in the public directory (use fetch instead of import) */
|
|
32
|
+
isPublic?: boolean;
|
|
28
33
|
}
|
|
29
34
|
|
|
30
35
|
/**
|
|
@@ -145,14 +150,18 @@ export function vitePlugin(config: EzI18nConfig): Plugin {
|
|
|
145
150
|
// Build translation info for each locale
|
|
146
151
|
for (const locale of finalLocales) {
|
|
147
152
|
const files = translations[locale] || [];
|
|
153
|
+
// Check if files are in public directory
|
|
154
|
+
const filesInPublic = files.length > 0 && isInPublicDir(files[0], projectRoot);
|
|
155
|
+
|
|
148
156
|
const info: TranslationInfo = {
|
|
149
157
|
locale,
|
|
150
158
|
files,
|
|
151
159
|
localeBaseDir: localeBaseDirs[locale],
|
|
160
|
+
isPublic: filesInPublic,
|
|
152
161
|
};
|
|
153
162
|
|
|
154
|
-
// For dev mode, determine if we can use import.meta.glob
|
|
155
|
-
if (isDev && config.translations) {
|
|
163
|
+
// For dev mode, determine if we can use import.meta.glob (not for public files)
|
|
164
|
+
if (isDev && config.translations && !filesInPublic) {
|
|
156
165
|
const localeConfig = typeof config.translations === 'string'
|
|
157
166
|
? path.join(config.translations, locale) + '/' // Trailing slash ensures detectPathType returns 'folder'
|
|
158
167
|
: config.translations[locale];
|
|
@@ -160,7 +169,7 @@ export function vitePlugin(config: EzI18nConfig): Plugin {
|
|
|
160
169
|
if (localeConfig && typeof localeConfig === 'string') {
|
|
161
170
|
const pathType = detectPathType(localeConfig);
|
|
162
171
|
if (pathType === 'folder' || pathType === 'glob') {
|
|
163
|
-
// Can use import.meta.glob for HMR
|
|
172
|
+
// Can use import.meta.glob for HMR (returns null for public files)
|
|
164
173
|
const basePath = pathType === 'glob'
|
|
165
174
|
? localeConfig
|
|
166
175
|
: toGlobPattern(path.resolve(projectRoot, localeConfig), projectRoot);
|
|
@@ -337,6 +346,7 @@ export function t(key, params) {
|
|
|
337
346
|
/**
|
|
338
347
|
* Generate the translations virtual module for dev mode.
|
|
339
348
|
* Uses import.meta.glob where possible for HMR support.
|
|
349
|
+
* Uses fetch() for files in public/ directory (with fs fallback for SSR).
|
|
340
350
|
*/
|
|
341
351
|
function generateDevTranslationsModule(
|
|
342
352
|
translationInfo: Map<string, TranslationInfo>,
|
|
@@ -345,6 +355,7 @@ function generateDevTranslationsModule(
|
|
|
345
355
|
): string {
|
|
346
356
|
const imports: string[] = [];
|
|
347
357
|
const loaderEntries: string[] = [];
|
|
358
|
+
let needsPublicLoader = false;
|
|
348
359
|
|
|
349
360
|
// Add deepMerge inline for runtime merging
|
|
350
361
|
imports.push(getDeepMergeCode());
|
|
@@ -358,10 +369,40 @@ function generateDevTranslationsModule(
|
|
|
358
369
|
if (info.files.length === 0) {
|
|
359
370
|
// No files - return empty object
|
|
360
371
|
loaderEntries.push(` ${JSON.stringify(locale)}: async () => ({})`);
|
|
372
|
+
} else if (info.isPublic) {
|
|
373
|
+
// Public directory files - use fetch in browser, fs in SSR
|
|
374
|
+
needsPublicLoader = true;
|
|
375
|
+
if (pathBasedNamespacing && info.localeBaseDir) {
|
|
376
|
+
const fileEntries = info.files.map(f => {
|
|
377
|
+
const url = toPublicUrl(f, projectRoot);
|
|
378
|
+
const absolutePath = f.replace(/\\/g, '/');
|
|
379
|
+
const namespace = getNamespaceFromPath(f, info.localeBaseDir!);
|
|
380
|
+
return `{ url: ${JSON.stringify(url)}, path: ${JSON.stringify(absolutePath)}, namespace: ${JSON.stringify(namespace)} }`;
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
loaderEntries.push(` ${JSON.stringify(locale)}: async () => {
|
|
384
|
+
const fileInfos = [${fileEntries.join(', ')}];
|
|
385
|
+
const responses = await Promise.all(fileInfos.map(f => __loadPublicJson(f.url, f.path)));
|
|
386
|
+
const wrapped = responses.map((content, i) => __wrapWithNamespace(fileInfos[i].namespace, content));
|
|
387
|
+
return __deepMerge({}, ...wrapped);
|
|
388
|
+
}`);
|
|
389
|
+
} else {
|
|
390
|
+
const fileEntries = info.files.map(f => {
|
|
391
|
+
const url = toPublicUrl(f, projectRoot);
|
|
392
|
+
const absolutePath = f.replace(/\\/g, '/');
|
|
393
|
+
return `{ url: ${JSON.stringify(url)}, path: ${JSON.stringify(absolutePath)} }`;
|
|
394
|
+
});
|
|
395
|
+
loaderEntries.push(` ${JSON.stringify(locale)}: async () => {
|
|
396
|
+
const files = [${fileEntries.join(', ')}];
|
|
397
|
+
const responses = await Promise.all(files.map(f => __loadPublicJson(f.url, f.path)));
|
|
398
|
+
if (responses.length === 1) return responses[0];
|
|
399
|
+
return __deepMerge({}, ...responses);
|
|
400
|
+
}`);
|
|
401
|
+
}
|
|
361
402
|
} else if (info.globPattern && pathBasedNamespacing && info.localeBaseDir) {
|
|
362
403
|
// Use import.meta.glob with namespace wrapping
|
|
363
404
|
const varName = `__${locale}Modules`;
|
|
364
|
-
const
|
|
405
|
+
const localeBaseDirForNs = getLocaleBaseDirForNamespace(info.localeBaseDir, projectRoot);
|
|
365
406
|
imports.push(
|
|
366
407
|
`const ${varName} = import.meta.glob(${JSON.stringify(info.globPattern)}, { eager: true, import: 'default' });`
|
|
367
408
|
);
|
|
@@ -369,10 +410,11 @@ function generateDevTranslationsModule(
|
|
|
369
410
|
loaderEntries.push(` ${JSON.stringify(locale)}: async () => {
|
|
370
411
|
const entries = Object.entries(${varName});
|
|
371
412
|
if (entries.length === 0) return {};
|
|
372
|
-
const localeBaseDir = ${JSON.stringify(
|
|
413
|
+
const localeBaseDir = ${JSON.stringify(localeBaseDirForNs)};
|
|
373
414
|
const wrapped = entries.map(([filePath, content]) => {
|
|
374
|
-
// Extract relative path from locale base dir
|
|
375
|
-
const
|
|
415
|
+
// Extract relative path from locale base dir - filePath starts with /
|
|
416
|
+
const normalizedPath = filePath.startsWith('/') ? filePath.slice(1) : filePath;
|
|
417
|
+
const relativePath = normalizedPath.replace(localeBaseDir + '/', '').replace(/\\.json$/i, '');
|
|
376
418
|
const namespace = relativePath.replace(/[\\/]/g, '.').replace(/\\.index$/, '');
|
|
377
419
|
return __wrapWithNamespace(namespace, content);
|
|
378
420
|
});
|
|
@@ -392,7 +434,7 @@ function generateDevTranslationsModule(
|
|
|
392
434
|
return __deepMerge({}, ...modules);
|
|
393
435
|
}`);
|
|
394
436
|
} else if (info.files.length === 1) {
|
|
395
|
-
// Single file
|
|
437
|
+
// Single file - use import
|
|
396
438
|
const relativePath = toRelativeImport(info.files[0], projectRoot);
|
|
397
439
|
if (pathBasedNamespacing && info.localeBaseDir) {
|
|
398
440
|
const namespace = getNamespaceFromPath(info.files[0], info.localeBaseDir);
|
|
@@ -434,6 +476,11 @@ function generateDevTranslationsModule(
|
|
|
434
476
|
}
|
|
435
477
|
}
|
|
436
478
|
|
|
479
|
+
// Add public loader helper if needed
|
|
480
|
+
if (needsPublicLoader) {
|
|
481
|
+
imports.push(getPublicLoaderCode());
|
|
482
|
+
}
|
|
483
|
+
|
|
437
484
|
return `
|
|
438
485
|
${imports.join('\n')}
|
|
439
486
|
|
|
@@ -465,6 +512,7 @@ export async function loadTranslations(locale) {
|
|
|
465
512
|
/**
|
|
466
513
|
* Generate the translations virtual module for production builds.
|
|
467
514
|
* Pre-resolves all imports for optimal bundling.
|
|
515
|
+
* Uses fetch() for files in public/ directory (with fs fallback for SSR).
|
|
468
516
|
*/
|
|
469
517
|
function generateBuildTranslationsModule(
|
|
470
518
|
translationInfo: Map<string, TranslationInfo>,
|
|
@@ -474,12 +522,51 @@ function generateBuildTranslationsModule(
|
|
|
474
522
|
const loaderEntries: string[] = [];
|
|
475
523
|
let needsDeepMerge = false;
|
|
476
524
|
let needsNamespaceWrapper = false;
|
|
525
|
+
let needsPublicLoader = false;
|
|
477
526
|
|
|
478
527
|
for (const [locale, info] of translationInfo) {
|
|
479
528
|
if (info.files.length === 0) {
|
|
480
529
|
loaderEntries.push(` ${JSON.stringify(locale)}: async () => ({})`);
|
|
530
|
+
} else if (info.isPublic) {
|
|
531
|
+
// Public directory files - use fetch in browser, fs in SSR
|
|
532
|
+
needsPublicLoader = true;
|
|
533
|
+
needsDeepMerge = info.files.length > 1;
|
|
534
|
+
if (pathBasedNamespacing && info.localeBaseDir) {
|
|
535
|
+
needsNamespaceWrapper = true;
|
|
536
|
+
const fileEntries = info.files.map(f => {
|
|
537
|
+
const url = toPublicUrl(f, projectRoot);
|
|
538
|
+
const absolutePath = f.replace(/\\/g, '/');
|
|
539
|
+
const namespace = getNamespaceFromPath(f, info.localeBaseDir!);
|
|
540
|
+
return `{ url: ${JSON.stringify(url)}, path: ${JSON.stringify(absolutePath)}, namespace: ${JSON.stringify(namespace)} }`;
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
loaderEntries.push(` ${JSON.stringify(locale)}: async () => {
|
|
544
|
+
const fileInfos = [${fileEntries.join(', ')}];
|
|
545
|
+
const responses = await Promise.all(fileInfos.map(f => __loadPublicJson(f.url, f.path)));
|
|
546
|
+
const wrapped = responses.map((content, i) => __wrapWithNamespace(fileInfos[i].namespace, content));
|
|
547
|
+
return __deepMerge({}, ...wrapped);
|
|
548
|
+
}`);
|
|
549
|
+
} else {
|
|
550
|
+
const fileEntries = info.files.map(f => {
|
|
551
|
+
const url = toPublicUrl(f, projectRoot);
|
|
552
|
+
const absolutePath = f.replace(/\\/g, '/');
|
|
553
|
+
return `{ url: ${JSON.stringify(url)}, path: ${JSON.stringify(absolutePath)} }`;
|
|
554
|
+
});
|
|
555
|
+
if (fileEntries.length === 1) {
|
|
556
|
+
const f = info.files[0];
|
|
557
|
+
const url = toPublicUrl(f, projectRoot);
|
|
558
|
+
const absolutePath = f.replace(/\\/g, '/');
|
|
559
|
+
loaderEntries.push(` ${JSON.stringify(locale)}: () => __loadPublicJson(${JSON.stringify(url)}, ${JSON.stringify(absolutePath)})`);
|
|
560
|
+
} else {
|
|
561
|
+
loaderEntries.push(` ${JSON.stringify(locale)}: async () => {
|
|
562
|
+
const files = [${fileEntries.join(', ')}];
|
|
563
|
+
const responses = await Promise.all(files.map(f => __loadPublicJson(f.url, f.path)));
|
|
564
|
+
return __deepMerge({}, ...responses);
|
|
565
|
+
}`);
|
|
566
|
+
}
|
|
567
|
+
}
|
|
481
568
|
} else if (info.files.length === 1) {
|
|
482
|
-
// Single file
|
|
569
|
+
// Single file - use import
|
|
483
570
|
const relativePath = toRelativeImport(info.files[0], projectRoot);
|
|
484
571
|
if (pathBasedNamespacing && info.localeBaseDir) {
|
|
485
572
|
needsNamespaceWrapper = true;
|
|
@@ -527,6 +614,7 @@ function generateBuildTranslationsModule(
|
|
|
527
614
|
const helperCode = [
|
|
528
615
|
needsDeepMerge ? getDeepMergeCode() : '',
|
|
529
616
|
needsNamespaceWrapper ? generateNamespaceWrapperCode() : '',
|
|
617
|
+
needsPublicLoader ? getPublicLoaderCode() : '',
|
|
530
618
|
].filter(Boolean).join('\n');
|
|
531
619
|
|
|
532
620
|
return `
|
|
@@ -576,6 +664,25 @@ function __deepMerge(target, ...sources) {
|
|
|
576
664
|
}`;
|
|
577
665
|
}
|
|
578
666
|
|
|
667
|
+
/**
|
|
668
|
+
* Inline public JSON loader for the virtual module.
|
|
669
|
+
* Uses fetch in browser, fs.readFileSync in SSR/Node.
|
|
670
|
+
*/
|
|
671
|
+
function getPublicLoaderCode(): string {
|
|
672
|
+
return `
|
|
673
|
+
async function __loadPublicJson(url, absolutePath) {
|
|
674
|
+
if (typeof window !== 'undefined') {
|
|
675
|
+
// Browser - use fetch with relative URL
|
|
676
|
+
return fetch(url).then(r => r.json());
|
|
677
|
+
} else {
|
|
678
|
+
// SSR/Node - read from filesystem
|
|
679
|
+
const fs = await import('node:fs');
|
|
680
|
+
const content = fs.readFileSync(absolutePath, 'utf-8');
|
|
681
|
+
return JSON.parse(content);
|
|
682
|
+
}
|
|
683
|
+
}`;
|
|
684
|
+
}
|
|
685
|
+
|
|
579
686
|
// Re-export resolveConfig for backwards compatibility
|
|
580
687
|
export function resolveConfig(config: EzI18nConfig): ResolvedEzI18nConfig {
|
|
581
688
|
// This is now a simplified version - full resolution happens in buildStart
|