@zachhandley/ez-i18n 0.2.0 → 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 CHANGED
@@ -435,6 +435,7 @@ export function t(key, params) {
435
435
  function generateDevTranslationsModule(translationInfo, projectRoot, pathBasedNamespacing) {
436
436
  const imports = [];
437
437
  const loaderEntries = [];
438
+ let needsPublicLoader = false;
438
439
  imports.push(getDeepMergeCode());
439
440
  if (pathBasedNamespacing) {
440
441
  imports.push(generateNamespaceWrapperCode());
@@ -443,24 +444,29 @@ function generateDevTranslationsModule(translationInfo, projectRoot, pathBasedNa
443
444
  if (info.files.length === 0) {
444
445
  loaderEntries.push(` ${JSON.stringify(locale)}: async () => ({})`);
445
446
  } else if (info.isPublic) {
447
+ needsPublicLoader = true;
446
448
  if (pathBasedNamespacing && info.localeBaseDir) {
447
- const localeBaseDirForNs = getLocaleBaseDirForNamespace(info.localeBaseDir, projectRoot);
448
449
  const fileEntries = info.files.map((f) => {
449
450
  const url = toPublicUrl(f, projectRoot);
451
+ const absolutePath = f.replace(/\\/g, "/");
450
452
  const namespace = getNamespaceFromPath(f, info.localeBaseDir);
451
- return `{ url: ${JSON.stringify(url)}, namespace: ${JSON.stringify(namespace)} }`;
453
+ return `{ url: ${JSON.stringify(url)}, path: ${JSON.stringify(absolutePath)}, namespace: ${JSON.stringify(namespace)} }`;
452
454
  });
453
455
  loaderEntries.push(` ${JSON.stringify(locale)}: async () => {
454
456
  const fileInfos = [${fileEntries.join(", ")}];
455
- const responses = await Promise.all(fileInfos.map(f => fetch(f.url).then(r => r.json())));
457
+ const responses = await Promise.all(fileInfos.map(f => __loadPublicJson(f.url, f.path)));
456
458
  const wrapped = responses.map((content, i) => __wrapWithNamespace(fileInfos[i].namespace, content));
457
459
  return __deepMerge({}, ...wrapped);
458
460
  }`);
459
461
  } else {
460
- const urls = info.files.map((f) => toPublicUrl(f, projectRoot));
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
+ });
461
467
  loaderEntries.push(` ${JSON.stringify(locale)}: async () => {
462
- const urls = ${JSON.stringify(urls)};
463
- const responses = await Promise.all(urls.map(url => fetch(url).then(r => r.json())));
468
+ const files = [${fileEntries.join(", ")}];
469
+ const responses = await Promise.all(files.map(f => __loadPublicJson(f.url, f.path)));
464
470
  if (responses.length === 1) return responses[0];
465
471
  return __deepMerge({}, ...responses);
466
472
  }`);
@@ -531,6 +537,9 @@ function generateDevTranslationsModule(translationInfo, projectRoot, pathBasedNa
531
537
  }
532
538
  }
533
539
  }
540
+ if (needsPublicLoader) {
541
+ imports.push(getPublicLoaderCode());
542
+ }
534
543
  return `
535
544
  ${imports.join("\n")}
536
545
 
@@ -562,32 +571,42 @@ function generateBuildTranslationsModule(translationInfo, projectRoot, pathBased
562
571
  const loaderEntries = [];
563
572
  let needsDeepMerge = false;
564
573
  let needsNamespaceWrapper = false;
574
+ let needsPublicLoader = false;
565
575
  for (const [locale, info] of translationInfo) {
566
576
  if (info.files.length === 0) {
567
577
  loaderEntries.push(` ${JSON.stringify(locale)}: async () => ({})`);
568
578
  } else if (info.isPublic) {
579
+ needsPublicLoader = true;
569
580
  needsDeepMerge = info.files.length > 1;
570
581
  if (pathBasedNamespacing && info.localeBaseDir) {
571
582
  needsNamespaceWrapper = true;
572
583
  const fileEntries = info.files.map((f) => {
573
584
  const url = toPublicUrl(f, projectRoot);
585
+ const absolutePath = f.replace(/\\/g, "/");
574
586
  const namespace = getNamespaceFromPath(f, info.localeBaseDir);
575
- return `{ url: ${JSON.stringify(url)}, namespace: ${JSON.stringify(namespace)} }`;
587
+ return `{ url: ${JSON.stringify(url)}, path: ${JSON.stringify(absolutePath)}, namespace: ${JSON.stringify(namespace)} }`;
576
588
  });
577
589
  loaderEntries.push(` ${JSON.stringify(locale)}: async () => {
578
590
  const fileInfos = [${fileEntries.join(", ")}];
579
- const responses = await Promise.all(fileInfos.map(f => fetch(f.url).then(r => r.json())));
591
+ const responses = await Promise.all(fileInfos.map(f => __loadPublicJson(f.url, f.path)));
580
592
  const wrapped = responses.map((content, i) => __wrapWithNamespace(fileInfos[i].namespace, content));
581
593
  return __deepMerge({}, ...wrapped);
582
594
  }`);
583
595
  } else {
584
- const urls = info.files.map((f) => toPublicUrl(f, projectRoot));
585
- if (urls.length === 1) {
586
- loaderEntries.push(` ${JSON.stringify(locale)}: () => fetch(${JSON.stringify(urls[0])}).then(r => r.json())`);
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)})`);
587
606
  } else {
588
607
  loaderEntries.push(` ${JSON.stringify(locale)}: async () => {
589
- const urls = ${JSON.stringify(urls)};
590
- const responses = await Promise.all(urls.map(url => fetch(url).then(r => r.json())));
608
+ const files = [${fileEntries.join(", ")}];
609
+ const responses = await Promise.all(files.map(f => __loadPublicJson(f.url, f.path)));
591
610
  return __deepMerge({}, ...responses);
592
611
  }`);
593
612
  }
@@ -632,7 +651,8 @@ function generateBuildTranslationsModule(translationInfo, projectRoot, pathBased
632
651
  }
633
652
  const helperCode = [
634
653
  needsDeepMerge ? getDeepMergeCode() : "",
635
- needsNamespaceWrapper ? generateNamespaceWrapperCode() : ""
654
+ needsNamespaceWrapper ? generateNamespaceWrapperCode() : "",
655
+ needsPublicLoader ? getPublicLoaderCode() : ""
636
656
  ].filter(Boolean).join("\n");
637
657
  return `
638
658
  ${helperCode}
@@ -676,6 +696,20 @@ function __deepMerge(target, ...sources) {
676
696
  return result;
677
697
  }`;
678
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
+ }
679
713
  function resolveConfig(config) {
680
714
  const isAutoDiscovery = !config.translations || typeof config.translations === "string";
681
715
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zachhandley/ez-i18n",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -346,7 +346,7 @@ export function t(key, params) {
346
346
  /**
347
347
  * Generate the translations virtual module for dev mode.
348
348
  * Uses import.meta.glob where possible for HMR support.
349
- * Uses fetch() for files in public/ directory.
349
+ * Uses fetch() for files in public/ directory (with fs fallback for SSR).
350
350
  */
351
351
  function generateDevTranslationsModule(
352
352
  translationInfo: Map<string, TranslationInfo>,
@@ -355,6 +355,7 @@ function generateDevTranslationsModule(
355
355
  ): string {
356
356
  const imports: string[] = [];
357
357
  const loaderEntries: string[] = [];
358
+ let needsPublicLoader = false;
358
359
 
359
360
  // Add deepMerge inline for runtime merging
360
361
  imports.push(getDeepMergeCode());
@@ -369,26 +370,31 @@ function generateDevTranslationsModule(
369
370
  // No files - return empty object
370
371
  loaderEntries.push(` ${JSON.stringify(locale)}: async () => ({})`);
371
372
  } else if (info.isPublic) {
372
- // Public directory files - use fetch() instead of import
373
+ // Public directory files - use fetch in browser, fs in SSR
374
+ needsPublicLoader = true;
373
375
  if (pathBasedNamespacing && info.localeBaseDir) {
374
- const localeBaseDirForNs = getLocaleBaseDirForNamespace(info.localeBaseDir, projectRoot);
375
376
  const fileEntries = info.files.map(f => {
376
377
  const url = toPublicUrl(f, projectRoot);
378
+ const absolutePath = f.replace(/\\/g, '/');
377
379
  const namespace = getNamespaceFromPath(f, info.localeBaseDir!);
378
- return `{ url: ${JSON.stringify(url)}, namespace: ${JSON.stringify(namespace)} }`;
380
+ return `{ url: ${JSON.stringify(url)}, path: ${JSON.stringify(absolutePath)}, namespace: ${JSON.stringify(namespace)} }`;
379
381
  });
380
382
 
381
383
  loaderEntries.push(` ${JSON.stringify(locale)}: async () => {
382
384
  const fileInfos = [${fileEntries.join(', ')}];
383
- const responses = await Promise.all(fileInfos.map(f => fetch(f.url).then(r => r.json())));
385
+ const responses = await Promise.all(fileInfos.map(f => __loadPublicJson(f.url, f.path)));
384
386
  const wrapped = responses.map((content, i) => __wrapWithNamespace(fileInfos[i].namespace, content));
385
387
  return __deepMerge({}, ...wrapped);
386
388
  }`);
387
389
  } else {
388
- const urls = info.files.map(f => toPublicUrl(f, projectRoot));
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
+ });
389
395
  loaderEntries.push(` ${JSON.stringify(locale)}: async () => {
390
- const urls = ${JSON.stringify(urls)};
391
- const responses = await Promise.all(urls.map(url => fetch(url).then(r => r.json())));
396
+ const files = [${fileEntries.join(', ')}];
397
+ const responses = await Promise.all(files.map(f => __loadPublicJson(f.url, f.path)));
392
398
  if (responses.length === 1) return responses[0];
393
399
  return __deepMerge({}, ...responses);
394
400
  }`);
@@ -470,6 +476,11 @@ function generateDevTranslationsModule(
470
476
  }
471
477
  }
472
478
 
479
+ // Add public loader helper if needed
480
+ if (needsPublicLoader) {
481
+ imports.push(getPublicLoaderCode());
482
+ }
483
+
473
484
  return `
474
485
  ${imports.join('\n')}
475
486
 
@@ -501,7 +512,7 @@ export async function loadTranslations(locale) {
501
512
  /**
502
513
  * Generate the translations virtual module for production builds.
503
514
  * Pre-resolves all imports for optimal bundling.
504
- * Uses fetch() for files in public/ directory.
515
+ * Uses fetch() for files in public/ directory (with fs fallback for SSR).
505
516
  */
506
517
  function generateBuildTranslationsModule(
507
518
  translationInfo: Map<string, TranslationInfo>,
@@ -511,35 +522,45 @@ function generateBuildTranslationsModule(
511
522
  const loaderEntries: string[] = [];
512
523
  let needsDeepMerge = false;
513
524
  let needsNamespaceWrapper = false;
525
+ let needsPublicLoader = false;
514
526
 
515
527
  for (const [locale, info] of translationInfo) {
516
528
  if (info.files.length === 0) {
517
529
  loaderEntries.push(` ${JSON.stringify(locale)}: async () => ({})`);
518
530
  } else if (info.isPublic) {
519
- // Public directory files - use fetch() instead of import
531
+ // Public directory files - use fetch in browser, fs in SSR
532
+ needsPublicLoader = true;
520
533
  needsDeepMerge = info.files.length > 1;
521
534
  if (pathBasedNamespacing && info.localeBaseDir) {
522
535
  needsNamespaceWrapper = true;
523
536
  const fileEntries = info.files.map(f => {
524
537
  const url = toPublicUrl(f, projectRoot);
538
+ const absolutePath = f.replace(/\\/g, '/');
525
539
  const namespace = getNamespaceFromPath(f, info.localeBaseDir!);
526
- return `{ url: ${JSON.stringify(url)}, namespace: ${JSON.stringify(namespace)} }`;
540
+ return `{ url: ${JSON.stringify(url)}, path: ${JSON.stringify(absolutePath)}, namespace: ${JSON.stringify(namespace)} }`;
527
541
  });
528
542
 
529
543
  loaderEntries.push(` ${JSON.stringify(locale)}: async () => {
530
544
  const fileInfos = [${fileEntries.join(', ')}];
531
- const responses = await Promise.all(fileInfos.map(f => fetch(f.url).then(r => r.json())));
545
+ const responses = await Promise.all(fileInfos.map(f => __loadPublicJson(f.url, f.path)));
532
546
  const wrapped = responses.map((content, i) => __wrapWithNamespace(fileInfos[i].namespace, content));
533
547
  return __deepMerge({}, ...wrapped);
534
548
  }`);
535
549
  } else {
536
- const urls = info.files.map(f => toPublicUrl(f, projectRoot));
537
- if (urls.length === 1) {
538
- loaderEntries.push(` ${JSON.stringify(locale)}: () => fetch(${JSON.stringify(urls[0])}).then(r => r.json())`);
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)})`);
539
560
  } else {
540
561
  loaderEntries.push(` ${JSON.stringify(locale)}: async () => {
541
- const urls = ${JSON.stringify(urls)};
542
- const responses = await Promise.all(urls.map(url => fetch(url).then(r => r.json())));
562
+ const files = [${fileEntries.join(', ')}];
563
+ const responses = await Promise.all(files.map(f => __loadPublicJson(f.url, f.path)));
543
564
  return __deepMerge({}, ...responses);
544
565
  }`);
545
566
  }
@@ -593,6 +614,7 @@ function generateBuildTranslationsModule(
593
614
  const helperCode = [
594
615
  needsDeepMerge ? getDeepMergeCode() : '',
595
616
  needsNamespaceWrapper ? generateNamespaceWrapperCode() : '',
617
+ needsPublicLoader ? getPublicLoaderCode() : '',
596
618
  ].filter(Boolean).join('\n');
597
619
 
598
620
  return `
@@ -642,6 +664,25 @@ function __deepMerge(target, ...sources) {
642
664
  }`;
643
665
  }
644
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
+
645
686
  // Re-export resolveConfig for backwards compatibility
646
687
  export function resolveConfig(config: EzI18nConfig): ResolvedEzI18nConfig {
647
688
  // This is now a simplified version - full resolution happens in buildStart