@zachhandley/ez-i18n 0.1.3 → 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 +40 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +151 -20
- package/dist/runtime/react-plugin.d.ts +1 -1
- package/dist/runtime/vue-plugin.d.ts +1 -1
- package/dist/{types-CHyDGt_C.d.ts → types-Cd9e7Lkc.d.ts} +12 -0
- package/dist/utils/index.d.ts +28 -2
- package/dist/utils/index.js +31 -1
- package/package.json +1 -1
- package/src/types.ts +17 -0
- package/src/utils/index.ts +3 -0
- package/src/utils/translations.ts +67 -0
- package/src/vite-plugin.ts +167 -25
package/README.md
CHANGED
|
@@ -125,6 +125,44 @@ en/
|
|
|
125
125
|
99-overrides.json # Loaded last, highest priority
|
|
126
126
|
```
|
|
127
127
|
|
|
128
|
+
#### Path-Based Namespacing
|
|
129
|
+
|
|
130
|
+
When using folder-based translation organization, ez-i18n automatically creates namespaces from your file paths. This is **enabled by default** when using folder-based config.
|
|
131
|
+
|
|
132
|
+
**Example:**
|
|
133
|
+
|
|
134
|
+
```
|
|
135
|
+
public/i18n/
|
|
136
|
+
en/
|
|
137
|
+
auth/
|
|
138
|
+
login.json # { "title": "Sign In", "button": "Log In" }
|
|
139
|
+
signup.json # { "title": "Create Account" }
|
|
140
|
+
common.json # { "welcome": "Welcome" }
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Access translations using dot notation that mirrors the folder structure:
|
|
144
|
+
|
|
145
|
+
```typescript
|
|
146
|
+
$t('auth.login.title') // "Sign In"
|
|
147
|
+
$t('auth.login.button') // "Log In"
|
|
148
|
+
$t('auth.signup.title') // "Create Account"
|
|
149
|
+
$t('common.welcome') // "Welcome"
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
**Disable path-based namespacing:**
|
|
153
|
+
|
|
154
|
+
If you prefer to manage namespaces manually within your JSON files, you can disable this feature:
|
|
155
|
+
|
|
156
|
+
```typescript
|
|
157
|
+
ezI18n({
|
|
158
|
+
defaultLocale: 'en',
|
|
159
|
+
translations: './src/i18n/',
|
|
160
|
+
pathBasedNamespacing: false, // Disable automatic path namespacing
|
|
161
|
+
})
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
With `pathBasedNamespacing: false`, the file structure is ignored and keys are used directly from each JSON file.
|
|
165
|
+
|
|
128
166
|
#### Cache File
|
|
129
167
|
|
|
130
168
|
A `.ez-i18n.json` cache file is generated to speed up subsequent builds. Add it to `.gitignore`:
|
|
@@ -248,6 +286,7 @@ function MyComponent() {
|
|
|
248
286
|
- **Middleware included** - Auto-detects locale from cookie, query param, or Accept-Language header
|
|
249
287
|
- **Multi-file support** - Organize translations in folders, use globs, or arrays
|
|
250
288
|
- **Auto-discovery** - Automatic locale detection from folder structure
|
|
289
|
+
- **Path-based namespacing** - Automatic namespacing from folder structure (`auth/login.json` becomes `auth.login.*`)
|
|
251
290
|
- **HMR in dev** - Hot reload translation changes without restart
|
|
252
291
|
|
|
253
292
|
## Locale Detection Priority
|
|
@@ -269,6 +308,7 @@ Astro integration function.
|
|
|
269
308
|
| `defaultLocale` | `string` | Yes | Fallback locale |
|
|
270
309
|
| `cookieName` | `string` | No | Cookie name (default: `'ez-locale'`) |
|
|
271
310
|
| `translations` | `string \| Record<string, TranslationPath>` | No | Base directory or per-locale paths (default: `./public/i18n/`) |
|
|
311
|
+
| `pathBasedNamespacing` | `boolean` | No | Auto-namespace translations from folder paths (default: `true` for folder-based config) |
|
|
272
312
|
|
|
273
313
|
**TranslationPath** can be:
|
|
274
314
|
- Single file: `'./src/i18n/en.json'`
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { AstroIntegration } from 'astro';
|
|
2
|
-
import { E as EzI18nConfig } from './types-
|
|
3
|
-
export { T as TranslateFunction } from './types-
|
|
2
|
+
import { E as EzI18nConfig } from './types-Cd9e7Lkc.js';
|
|
3
|
+
export { T as TranslateFunction } from './types-Cd9e7Lkc.js';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* ez-i18n Astro integration
|
package/dist/index.js
CHANGED
|
@@ -155,6 +155,24 @@ function toGlobPattern(baseDir, projectRoot) {
|
|
|
155
155
|
const normalized = relativePath.startsWith(".") ? relativePath : "./" + relativePath;
|
|
156
156
|
return `${normalized}/**/*.json`;
|
|
157
157
|
}
|
|
158
|
+
function getNamespaceFromPath(filePath, localeDir) {
|
|
159
|
+
const relative2 = path.relative(localeDir, filePath);
|
|
160
|
+
const withoutExt = relative2.replace(/\.json$/i, "");
|
|
161
|
+
const namespace = withoutExt.replace(/[\\/]/g, ".");
|
|
162
|
+
return namespace.replace(/\.index$/, "");
|
|
163
|
+
}
|
|
164
|
+
function generateNamespaceWrapperCode() {
|
|
165
|
+
return `
|
|
166
|
+
function __wrapWithNamespace(namespace, content) {
|
|
167
|
+
if (!namespace) return content;
|
|
168
|
+
const parts = namespace.split('.');
|
|
169
|
+
let result = content;
|
|
170
|
+
for (let i = parts.length - 1; i >= 0; i--) {
|
|
171
|
+
result = { [parts[i]]: result };
|
|
172
|
+
}
|
|
173
|
+
return result;
|
|
174
|
+
}`;
|
|
175
|
+
}
|
|
158
176
|
|
|
159
177
|
// src/vite-plugin.ts
|
|
160
178
|
import * as path2 from "path";
|
|
@@ -176,19 +194,32 @@ function vitePlugin(config) {
|
|
|
176
194
|
},
|
|
177
195
|
async buildStart() {
|
|
178
196
|
const projectRoot = viteConfig.root;
|
|
197
|
+
const isAutoDiscovery = !config.translations || typeof config.translations === "string";
|
|
198
|
+
const pathBasedNamespacing = config.pathBasedNamespacing ?? isAutoDiscovery;
|
|
199
|
+
const translationsBaseDir = typeof config.translations === "string" ? path2.resolve(projectRoot, config.translations.replace(/\/$/, "")) : path2.resolve(projectRoot, "./public/i18n");
|
|
179
200
|
let useCache = false;
|
|
180
201
|
if (!isDev) {
|
|
181
202
|
const cache = loadCache(projectRoot);
|
|
182
203
|
if (cache && isCacheValid(cache, projectRoot)) {
|
|
204
|
+
const localeBaseDirs = {};
|
|
205
|
+
for (const locale of Object.keys(cache.discovered)) {
|
|
206
|
+
localeBaseDirs[locale] = path2.join(translationsBaseDir, locale);
|
|
207
|
+
}
|
|
183
208
|
resolved = {
|
|
184
209
|
locales: config.locales || Object.keys(cache.discovered),
|
|
185
210
|
defaultLocale: config.defaultLocale,
|
|
186
211
|
cookieName: config.cookieName ?? "ez-locale",
|
|
187
|
-
translations: cache.discovered
|
|
212
|
+
translations: cache.discovered,
|
|
213
|
+
pathBasedNamespacing,
|
|
214
|
+
localeBaseDirs
|
|
188
215
|
};
|
|
189
216
|
useCache = true;
|
|
190
217
|
for (const [locale, files] of Object.entries(cache.discovered)) {
|
|
191
|
-
translationInfo.set(locale, {
|
|
218
|
+
translationInfo.set(locale, {
|
|
219
|
+
locale,
|
|
220
|
+
files,
|
|
221
|
+
localeBaseDir: localeBaseDirs[locale]
|
|
222
|
+
});
|
|
192
223
|
}
|
|
193
224
|
}
|
|
194
225
|
}
|
|
@@ -199,15 +230,43 @@ function vitePlugin(config) {
|
|
|
199
230
|
config.locales
|
|
200
231
|
);
|
|
201
232
|
const finalLocales = config.locales && config.locales.length > 0 ? config.locales : locales;
|
|
233
|
+
const localeBaseDirs = {};
|
|
234
|
+
for (const locale of finalLocales) {
|
|
235
|
+
if (isAutoDiscovery) {
|
|
236
|
+
localeBaseDirs[locale] = path2.join(translationsBaseDir, locale);
|
|
237
|
+
} else if (typeof config.translations === "object" && config.translations[locale]) {
|
|
238
|
+
const localeConfig = config.translations[locale];
|
|
239
|
+
if (typeof localeConfig === "string") {
|
|
240
|
+
const pathType = detectPathType(localeConfig);
|
|
241
|
+
if (pathType === "folder" || pathType === "file") {
|
|
242
|
+
const resolved2 = path2.resolve(projectRoot, localeConfig.replace(/\/$/, ""));
|
|
243
|
+
localeBaseDirs[locale] = pathType === "folder" ? resolved2 : path2.dirname(resolved2);
|
|
244
|
+
} else {
|
|
245
|
+
const baseDir = localeConfig.split("*")[0].replace(/\/$/, "");
|
|
246
|
+
localeBaseDirs[locale] = path2.resolve(projectRoot, baseDir);
|
|
247
|
+
}
|
|
248
|
+
} else {
|
|
249
|
+
localeBaseDirs[locale] = translationsBaseDir;
|
|
250
|
+
}
|
|
251
|
+
} else {
|
|
252
|
+
localeBaseDirs[locale] = translationsBaseDir;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
202
255
|
resolved = {
|
|
203
256
|
locales: finalLocales,
|
|
204
257
|
defaultLocale: config.defaultLocale,
|
|
205
258
|
cookieName: config.cookieName ?? "ez-locale",
|
|
206
|
-
translations
|
|
259
|
+
translations,
|
|
260
|
+
pathBasedNamespacing,
|
|
261
|
+
localeBaseDirs
|
|
207
262
|
};
|
|
208
263
|
for (const locale of finalLocales) {
|
|
209
264
|
const files = translations[locale] || [];
|
|
210
|
-
const info = {
|
|
265
|
+
const info = {
|
|
266
|
+
locale,
|
|
267
|
+
files,
|
|
268
|
+
localeBaseDir: localeBaseDirs[locale]
|
|
269
|
+
};
|
|
211
270
|
if (isDev && config.translations) {
|
|
212
271
|
const localeConfig = typeof config.translations === "string" ? path2.join(config.translations, locale) : config.translations[locale];
|
|
213
272
|
if (localeConfig && typeof localeConfig === "string") {
|
|
@@ -295,7 +354,7 @@ export function t(key, params) {
|
|
|
295
354
|
`;
|
|
296
355
|
}
|
|
297
356
|
if (id === RESOLVED_PREFIX + VIRTUAL_TRANSLATIONS) {
|
|
298
|
-
return isDev ? generateDevTranslationsModule(translationInfo, viteConfig.root) : generateBuildTranslationsModule(translationInfo, viteConfig.root);
|
|
357
|
+
return isDev ? generateDevTranslationsModule(translationInfo, viteConfig.root, resolved.pathBasedNamespacing) : generateBuildTranslationsModule(translationInfo, viteConfig.root, resolved.pathBasedNamespacing);
|
|
299
358
|
}
|
|
300
359
|
return null;
|
|
301
360
|
},
|
|
@@ -350,13 +409,34 @@ export function t(key, params) {
|
|
|
350
409
|
}
|
|
351
410
|
};
|
|
352
411
|
}
|
|
353
|
-
function generateDevTranslationsModule(translationInfo, projectRoot) {
|
|
412
|
+
function generateDevTranslationsModule(translationInfo, projectRoot, pathBasedNamespacing) {
|
|
354
413
|
const imports = [];
|
|
355
414
|
const loaderEntries = [];
|
|
356
415
|
imports.push(getDeepMergeCode());
|
|
416
|
+
if (pathBasedNamespacing) {
|
|
417
|
+
imports.push(generateNamespaceWrapperCode());
|
|
418
|
+
}
|
|
357
419
|
for (const [locale, info] of translationInfo) {
|
|
358
420
|
if (info.files.length === 0) {
|
|
359
421
|
loaderEntries.push(` ${JSON.stringify(locale)}: async () => ({})`);
|
|
422
|
+
} else if (info.globPattern && pathBasedNamespacing && info.localeBaseDir) {
|
|
423
|
+
const varName = `__${locale}Modules`;
|
|
424
|
+
const localeBaseDir = info.localeBaseDir.replace(/\\/g, "/");
|
|
425
|
+
imports.push(
|
|
426
|
+
`const ${varName} = import.meta.glob(${JSON.stringify(info.globPattern)}, { eager: true, import: 'default' });`
|
|
427
|
+
);
|
|
428
|
+
loaderEntries.push(` ${JSON.stringify(locale)}: async () => {
|
|
429
|
+
const entries = Object.entries(${varName});
|
|
430
|
+
if (entries.length === 0) return {};
|
|
431
|
+
const localeBaseDir = ${JSON.stringify(localeBaseDir)};
|
|
432
|
+
const wrapped = entries.map(([filePath, content]) => {
|
|
433
|
+
// Extract relative path from locale base dir
|
|
434
|
+
const relativePath = filePath.replace(localeBaseDir + '/', '').replace(/\\.json$/i, '');
|
|
435
|
+
const namespace = relativePath.replace(/[\\/]/g, '.').replace(/\\.index$/, '');
|
|
436
|
+
return __wrapWithNamespace(namespace, content);
|
|
437
|
+
});
|
|
438
|
+
return __deepMerge({}, ...wrapped);
|
|
439
|
+
}`);
|
|
360
440
|
} else if (info.globPattern) {
|
|
361
441
|
const varName = `__${locale}Modules`;
|
|
362
442
|
imports.push(
|
|
@@ -370,17 +450,38 @@ function generateDevTranslationsModule(translationInfo, projectRoot) {
|
|
|
370
450
|
}`);
|
|
371
451
|
} else if (info.files.length === 1) {
|
|
372
452
|
const relativePath = toRelativeImport(info.files[0], projectRoot);
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
453
|
+
if (pathBasedNamespacing && info.localeBaseDir) {
|
|
454
|
+
const namespace = getNamespaceFromPath(info.files[0], info.localeBaseDir);
|
|
455
|
+
loaderEntries.push(
|
|
456
|
+
` ${JSON.stringify(locale)}: () => import(${JSON.stringify(relativePath)}).then(m => __wrapWithNamespace(${JSON.stringify(namespace)}, m.default ?? m))`
|
|
457
|
+
);
|
|
458
|
+
} else {
|
|
459
|
+
loaderEntries.push(
|
|
460
|
+
` ${JSON.stringify(locale)}: () => import(${JSON.stringify(relativePath)}).then(m => m.default ?? m)`
|
|
461
|
+
);
|
|
462
|
+
}
|
|
376
463
|
} else {
|
|
377
|
-
|
|
378
|
-
|
|
464
|
+
if (pathBasedNamespacing && info.localeBaseDir) {
|
|
465
|
+
const fileEntries = info.files.map((f) => {
|
|
466
|
+
const relativePath = toRelativeImport(f, projectRoot);
|
|
467
|
+
const namespace = getNamespaceFromPath(f, info.localeBaseDir);
|
|
468
|
+
return `{ path: ${JSON.stringify(relativePath)}, namespace: ${JSON.stringify(namespace)} }`;
|
|
469
|
+
});
|
|
470
|
+
loaderEntries.push(` ${JSON.stringify(locale)}: async () => {
|
|
471
|
+
const fileInfos = [${fileEntries.join(", ")}];
|
|
472
|
+
const modules = await Promise.all(fileInfos.map(f => import(f.path)));
|
|
473
|
+
const wrapped = modules.map((m, i) => __wrapWithNamespace(fileInfos[i].namespace, m.default ?? m));
|
|
474
|
+
return __deepMerge({}, ...wrapped);
|
|
475
|
+
}`);
|
|
476
|
+
} else {
|
|
477
|
+
const fileImports = info.files.map((f) => `import(${JSON.stringify(toRelativeImport(f, projectRoot))})`).join(", ");
|
|
478
|
+
loaderEntries.push(` ${JSON.stringify(locale)}: async () => {
|
|
379
479
|
const modules = await Promise.all([${fileImports}]);
|
|
380
480
|
const contents = modules.map(m => m.default ?? m);
|
|
381
481
|
if (contents.length === 1) return contents[0];
|
|
382
482
|
return __deepMerge({}, ...contents);
|
|
383
483
|
}`);
|
|
484
|
+
}
|
|
384
485
|
}
|
|
385
486
|
}
|
|
386
487
|
return `
|
|
@@ -410,30 +511,57 @@ export async function loadTranslations(locale) {
|
|
|
410
511
|
}
|
|
411
512
|
`;
|
|
412
513
|
}
|
|
413
|
-
function generateBuildTranslationsModule(translationInfo, projectRoot) {
|
|
514
|
+
function generateBuildTranslationsModule(translationInfo, projectRoot, pathBasedNamespacing) {
|
|
414
515
|
const loaderEntries = [];
|
|
415
516
|
let needsDeepMerge = false;
|
|
517
|
+
let needsNamespaceWrapper = false;
|
|
416
518
|
for (const [locale, info] of translationInfo) {
|
|
417
519
|
if (info.files.length === 0) {
|
|
418
520
|
loaderEntries.push(` ${JSON.stringify(locale)}: async () => ({})`);
|
|
419
521
|
} else if (info.files.length === 1) {
|
|
420
522
|
const relativePath = toRelativeImport(info.files[0], projectRoot);
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
523
|
+
if (pathBasedNamespacing && info.localeBaseDir) {
|
|
524
|
+
needsNamespaceWrapper = true;
|
|
525
|
+
const namespace = getNamespaceFromPath(info.files[0], info.localeBaseDir);
|
|
526
|
+
loaderEntries.push(
|
|
527
|
+
` ${JSON.stringify(locale)}: () => import(${JSON.stringify(relativePath)}).then(m => __wrapWithNamespace(${JSON.stringify(namespace)}, m.default ?? m))`
|
|
528
|
+
);
|
|
529
|
+
} else {
|
|
530
|
+
loaderEntries.push(
|
|
531
|
+
` ${JSON.stringify(locale)}: () => import(${JSON.stringify(relativePath)}).then(m => m.default ?? m)`
|
|
532
|
+
);
|
|
533
|
+
}
|
|
424
534
|
} else {
|
|
425
535
|
needsDeepMerge = true;
|
|
426
|
-
|
|
427
|
-
|
|
536
|
+
if (pathBasedNamespacing && info.localeBaseDir) {
|
|
537
|
+
needsNamespaceWrapper = true;
|
|
538
|
+
const fileEntries = info.files.map((f) => {
|
|
539
|
+
const relativePath = toRelativeImport(f, projectRoot);
|
|
540
|
+
const namespace = getNamespaceFromPath(f, info.localeBaseDir);
|
|
541
|
+
return `{ path: ${JSON.stringify(relativePath)}, namespace: ${JSON.stringify(namespace)} }`;
|
|
542
|
+
});
|
|
543
|
+
loaderEntries.push(` ${JSON.stringify(locale)}: async () => {
|
|
544
|
+
const fileInfos = [${fileEntries.join(", ")}];
|
|
545
|
+
const modules = await Promise.all(fileInfos.map(f => import(f.path)));
|
|
546
|
+
const wrapped = modules.map((m, i) => __wrapWithNamespace(fileInfos[i].namespace, m.default ?? m));
|
|
547
|
+
return __deepMerge({}, ...wrapped);
|
|
548
|
+
}`);
|
|
549
|
+
} else {
|
|
550
|
+
const fileImports = info.files.map((f) => `import(${JSON.stringify(toRelativeImport(f, projectRoot))})`).join(", ");
|
|
551
|
+
loaderEntries.push(` ${JSON.stringify(locale)}: async () => {
|
|
428
552
|
const modules = await Promise.all([${fileImports}]);
|
|
429
553
|
const contents = modules.map(m => m.default ?? m);
|
|
430
554
|
return __deepMerge({}, ...contents);
|
|
431
555
|
}`);
|
|
556
|
+
}
|
|
432
557
|
}
|
|
433
558
|
}
|
|
434
|
-
const
|
|
559
|
+
const helperCode = [
|
|
560
|
+
needsDeepMerge ? getDeepMergeCode() : "",
|
|
561
|
+
needsNamespaceWrapper ? generateNamespaceWrapperCode() : ""
|
|
562
|
+
].filter(Boolean).join("\n");
|
|
435
563
|
return `
|
|
436
|
-
${
|
|
564
|
+
${helperCode}
|
|
437
565
|
|
|
438
566
|
export const translationLoaders = {
|
|
439
567
|
${loaderEntries.join(",\n")}
|
|
@@ -475,11 +603,14 @@ function __deepMerge(target, ...sources) {
|
|
|
475
603
|
}`;
|
|
476
604
|
}
|
|
477
605
|
function resolveConfig(config) {
|
|
606
|
+
const isAutoDiscovery = !config.translations || typeof config.translations === "string";
|
|
478
607
|
return {
|
|
479
608
|
locales: config.locales || [],
|
|
480
609
|
defaultLocale: config.defaultLocale,
|
|
481
610
|
cookieName: config.cookieName ?? "ez-locale",
|
|
482
|
-
translations: {}
|
|
611
|
+
translations: {},
|
|
612
|
+
pathBasedNamespacing: config.pathBasedNamespacing ?? isAutoDiscovery,
|
|
613
|
+
localeBaseDirs: {}
|
|
483
614
|
};
|
|
484
615
|
}
|
|
485
616
|
|
|
@@ -53,6 +53,18 @@ interface EzI18nConfig {
|
|
|
53
53
|
* If not specified, auto-discovers from ./public/i18n/
|
|
54
54
|
*/
|
|
55
55
|
translations?: TranslationsConfig;
|
|
56
|
+
/**
|
|
57
|
+
* Derive namespace from file path relative to locale folder.
|
|
58
|
+
*
|
|
59
|
+
* When enabled:
|
|
60
|
+
* - `en/auth/login.json` with `{ "title": "..." }` → `$t('auth.login.title')`
|
|
61
|
+
* - `en/common.json` with `{ "actions": {...} }` → `$t('common.actions.save')`
|
|
62
|
+
*
|
|
63
|
+
* The file path (minus locale folder and .json extension) becomes the key prefix.
|
|
64
|
+
*
|
|
65
|
+
* @default true when using folder-based translations config
|
|
66
|
+
*/
|
|
67
|
+
pathBasedNamespacing?: boolean;
|
|
56
68
|
}
|
|
57
69
|
/**
|
|
58
70
|
* Cache file structure (.ez-i18n.json)
|
package/dist/utils/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { L as LocaleTranslationPath, a as TranslationsConfig, b as TranslationCache } from '../types-
|
|
1
|
+
import { L as LocaleTranslationPath, a as TranslationsConfig, b as TranslationCache } from '../types-Cd9e7Lkc.js';
|
|
2
2
|
|
|
3
3
|
type PathType = 'file' | 'folder' | 'glob' | 'array';
|
|
4
4
|
/**
|
|
@@ -55,5 +55,31 @@ declare function toRelativeImport(absolutePath: string, projectRoot: string): st
|
|
|
55
55
|
* Generate a glob pattern for import.meta.glob from a base directory
|
|
56
56
|
*/
|
|
57
57
|
declare function toGlobPattern(baseDir: string, projectRoot: string): string;
|
|
58
|
+
/**
|
|
59
|
+
* Get namespace from file path relative to locale base directory.
|
|
60
|
+
*
|
|
61
|
+
* Examples:
|
|
62
|
+
* - filePath: /project/public/i18n/en/auth/login.json, localeDir: /project/public/i18n/en
|
|
63
|
+
* → namespace: 'auth.login'
|
|
64
|
+
* - filePath: /project/public/i18n/en/common.json, localeDir: /project/public/i18n/en
|
|
65
|
+
* → namespace: 'common'
|
|
66
|
+
* - filePath: /project/public/i18n/en/settings/index.json, localeDir: /project/public/i18n/en
|
|
67
|
+
* → namespace: 'settings' (index is stripped)
|
|
68
|
+
*/
|
|
69
|
+
declare function getNamespaceFromPath(filePath: string, localeDir: string): string;
|
|
70
|
+
/**
|
|
71
|
+
* Wrap a translation object with its namespace.
|
|
72
|
+
*
|
|
73
|
+
* Example:
|
|
74
|
+
* - namespace: 'auth.login'
|
|
75
|
+
* - content: { title: 'Welcome', subtitle: 'Login here' }
|
|
76
|
+
* - result: { auth: { login: { title: 'Welcome', subtitle: 'Login here' } } }
|
|
77
|
+
*/
|
|
78
|
+
declare function wrapWithNamespace(namespace: string, content: Record<string, unknown>): Record<string, unknown>;
|
|
79
|
+
/**
|
|
80
|
+
* Generate code that wraps imported content with namespace at runtime.
|
|
81
|
+
* Used in virtual module generation.
|
|
82
|
+
*/
|
|
83
|
+
declare function generateNamespaceWrapperCode(): string;
|
|
58
84
|
|
|
59
|
-
export { type PathType, autoDiscoverTranslations, deepMerge, detectPathType, isCacheValid, loadCache, resolveTranslationPaths, resolveTranslationsConfig, saveCache, toGlobPattern, toRelativeImport };
|
|
85
|
+
export { type PathType, autoDiscoverTranslations, deepMerge, detectPathType, generateNamespaceWrapperCode, getNamespaceFromPath, isCacheValid, loadCache, resolveTranslationPaths, resolveTranslationsConfig, saveCache, toGlobPattern, toRelativeImport, wrapWithNamespace };
|
package/dist/utils/index.js
CHANGED
|
@@ -176,15 +176,45 @@ function toGlobPattern(baseDir, projectRoot) {
|
|
|
176
176
|
const normalized = relativePath.startsWith(".") ? relativePath : "./" + relativePath;
|
|
177
177
|
return `${normalized}/**/*.json`;
|
|
178
178
|
}
|
|
179
|
+
function getNamespaceFromPath(filePath, localeDir) {
|
|
180
|
+
const relative2 = path.relative(localeDir, filePath);
|
|
181
|
+
const withoutExt = relative2.replace(/\.json$/i, "");
|
|
182
|
+
const namespace = withoutExt.replace(/[\\/]/g, ".");
|
|
183
|
+
return namespace.replace(/\.index$/, "");
|
|
184
|
+
}
|
|
185
|
+
function wrapWithNamespace(namespace, content) {
|
|
186
|
+
if (!namespace) return content;
|
|
187
|
+
const parts = namespace.split(".");
|
|
188
|
+
let result = content;
|
|
189
|
+
for (let i = parts.length - 1; i >= 0; i--) {
|
|
190
|
+
result = { [parts[i]]: result };
|
|
191
|
+
}
|
|
192
|
+
return result;
|
|
193
|
+
}
|
|
194
|
+
function generateNamespaceWrapperCode() {
|
|
195
|
+
return `
|
|
196
|
+
function __wrapWithNamespace(namespace, content) {
|
|
197
|
+
if (!namespace) return content;
|
|
198
|
+
const parts = namespace.split('.');
|
|
199
|
+
let result = content;
|
|
200
|
+
for (let i = parts.length - 1; i >= 0; i--) {
|
|
201
|
+
result = { [parts[i]]: result };
|
|
202
|
+
}
|
|
203
|
+
return result;
|
|
204
|
+
}`;
|
|
205
|
+
}
|
|
179
206
|
export {
|
|
180
207
|
autoDiscoverTranslations,
|
|
181
208
|
deepMerge,
|
|
182
209
|
detectPathType,
|
|
210
|
+
generateNamespaceWrapperCode,
|
|
211
|
+
getNamespaceFromPath,
|
|
183
212
|
isCacheValid,
|
|
184
213
|
loadCache,
|
|
185
214
|
resolveTranslationPaths,
|
|
186
215
|
resolveTranslationsConfig,
|
|
187
216
|
saveCache,
|
|
188
217
|
toGlobPattern,
|
|
189
|
-
toRelativeImport
|
|
218
|
+
toRelativeImport,
|
|
219
|
+
wrapWithNamespace
|
|
190
220
|
};
|
package/package.json
CHANGED
package/src/types.ts
CHANGED
|
@@ -58,6 +58,19 @@ export interface EzI18nConfig {
|
|
|
58
58
|
* If not specified, auto-discovers from ./public/i18n/
|
|
59
59
|
*/
|
|
60
60
|
translations?: TranslationsConfig;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Derive namespace from file path relative to locale folder.
|
|
64
|
+
*
|
|
65
|
+
* When enabled:
|
|
66
|
+
* - `en/auth/login.json` with `{ "title": "..." }` → `$t('auth.login.title')`
|
|
67
|
+
* - `en/common.json` with `{ "actions": {...} }` → `$t('common.actions.save')`
|
|
68
|
+
*
|
|
69
|
+
* The file path (minus locale folder and .json extension) becomes the key prefix.
|
|
70
|
+
*
|
|
71
|
+
* @default true when using folder-based translations config
|
|
72
|
+
*/
|
|
73
|
+
pathBasedNamespacing?: boolean;
|
|
61
74
|
}
|
|
62
75
|
|
|
63
76
|
/**
|
|
@@ -72,6 +85,10 @@ export interface ResolvedEzI18nConfig {
|
|
|
72
85
|
cookieName: string;
|
|
73
86
|
/** Normalized: locale → array of resolved absolute file paths */
|
|
74
87
|
translations: Record<string, string[]>;
|
|
88
|
+
/** Whether to derive namespace from file path */
|
|
89
|
+
pathBasedNamespacing: boolean;
|
|
90
|
+
/** Base directory for each locale (used for namespace calculation) */
|
|
91
|
+
localeBaseDirs: Record<string, string>;
|
|
75
92
|
}
|
|
76
93
|
|
|
77
94
|
/**
|
package/src/utils/index.ts
CHANGED
|
@@ -309,3 +309,70 @@ export function toGlobPattern(baseDir: string, projectRoot: string): string {
|
|
|
309
309
|
const normalized = relativePath.startsWith('.') ? relativePath : './' + relativePath;
|
|
310
310
|
return `${normalized}/**/*.json`;
|
|
311
311
|
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Get namespace from file path relative to locale base directory.
|
|
315
|
+
*
|
|
316
|
+
* Examples:
|
|
317
|
+
* - filePath: /project/public/i18n/en/auth/login.json, localeDir: /project/public/i18n/en
|
|
318
|
+
* → namespace: 'auth.login'
|
|
319
|
+
* - filePath: /project/public/i18n/en/common.json, localeDir: /project/public/i18n/en
|
|
320
|
+
* → namespace: 'common'
|
|
321
|
+
* - filePath: /project/public/i18n/en/settings/index.json, localeDir: /project/public/i18n/en
|
|
322
|
+
* → namespace: 'settings' (index is stripped)
|
|
323
|
+
*/
|
|
324
|
+
export function getNamespaceFromPath(filePath: string, localeDir: string): string {
|
|
325
|
+
// Get relative path from locale directory
|
|
326
|
+
const relative = path.relative(localeDir, filePath);
|
|
327
|
+
|
|
328
|
+
// Remove .json extension
|
|
329
|
+
const withoutExt = relative.replace(/\.json$/i, '');
|
|
330
|
+
|
|
331
|
+
// Convert path separators to dots
|
|
332
|
+
const namespace = withoutExt.replace(/[\\/]/g, '.');
|
|
333
|
+
|
|
334
|
+
// Remove trailing .index (index.json files represent the folder itself)
|
|
335
|
+
return namespace.replace(/\.index$/, '');
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Wrap a translation object with its namespace.
|
|
340
|
+
*
|
|
341
|
+
* Example:
|
|
342
|
+
* - namespace: 'auth.login'
|
|
343
|
+
* - content: { title: 'Welcome', subtitle: 'Login here' }
|
|
344
|
+
* - result: { auth: { login: { title: 'Welcome', subtitle: 'Login here' } } }
|
|
345
|
+
*/
|
|
346
|
+
export function wrapWithNamespace(
|
|
347
|
+
namespace: string,
|
|
348
|
+
content: Record<string, unknown>
|
|
349
|
+
): Record<string, unknown> {
|
|
350
|
+
if (!namespace) return content;
|
|
351
|
+
|
|
352
|
+
const parts = namespace.split('.');
|
|
353
|
+
let result: Record<string, unknown> = content;
|
|
354
|
+
|
|
355
|
+
// Build from inside out
|
|
356
|
+
for (let i = parts.length - 1; i >= 0; i--) {
|
|
357
|
+
result = { [parts[i]]: result };
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
return result;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Generate code that wraps imported content with namespace at runtime.
|
|
365
|
+
* Used in virtual module generation.
|
|
366
|
+
*/
|
|
367
|
+
export function generateNamespaceWrapperCode(): string {
|
|
368
|
+
return `
|
|
369
|
+
function __wrapWithNamespace(namespace, content) {
|
|
370
|
+
if (!namespace) return content;
|
|
371
|
+
const parts = namespace.split('.');
|
|
372
|
+
let result = content;
|
|
373
|
+
for (let i = parts.length - 1; i >= 0; i--) {
|
|
374
|
+
result = { [parts[i]]: result };
|
|
375
|
+
}
|
|
376
|
+
return result;
|
|
377
|
+
}`;
|
|
378
|
+
}
|
package/src/vite-plugin.ts
CHANGED
|
@@ -8,6 +8,8 @@ import {
|
|
|
8
8
|
saveCache,
|
|
9
9
|
isCacheValid,
|
|
10
10
|
detectPathType,
|
|
11
|
+
getNamespaceFromPath,
|
|
12
|
+
generateNamespaceWrapperCode,
|
|
11
13
|
} from './utils/translations';
|
|
12
14
|
import * as path from 'node:path';
|
|
13
15
|
|
|
@@ -21,6 +23,8 @@ interface TranslationInfo {
|
|
|
21
23
|
files: string[];
|
|
22
24
|
/** Glob pattern for dev mode HMR (if applicable) */
|
|
23
25
|
globPattern?: string;
|
|
26
|
+
/** Base directory for this locale (used for namespace calculation) */
|
|
27
|
+
localeBaseDir?: string;
|
|
24
28
|
}
|
|
25
29
|
|
|
26
30
|
/**
|
|
@@ -44,23 +48,45 @@ export function vitePlugin(config: EzI18nConfig): Plugin {
|
|
|
44
48
|
async buildStart() {
|
|
45
49
|
const projectRoot = viteConfig.root;
|
|
46
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
|
+
|
|
47
61
|
// Try to use cache in production builds
|
|
48
62
|
let useCache = false;
|
|
49
63
|
if (!isDev) {
|
|
50
64
|
const cache = loadCache(projectRoot);
|
|
51
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
|
+
|
|
52
72
|
// Use cached discovery
|
|
53
73
|
resolved = {
|
|
54
74
|
locales: config.locales || Object.keys(cache.discovered),
|
|
55
75
|
defaultLocale: config.defaultLocale,
|
|
56
76
|
cookieName: config.cookieName ?? 'ez-locale',
|
|
57
77
|
translations: cache.discovered,
|
|
78
|
+
pathBasedNamespacing,
|
|
79
|
+
localeBaseDirs,
|
|
58
80
|
};
|
|
59
81
|
useCache = true;
|
|
60
82
|
|
|
61
83
|
// Populate translationInfo from cache
|
|
62
84
|
for (const [locale, files] of Object.entries(cache.discovered)) {
|
|
63
|
-
translationInfo.set(locale, {
|
|
85
|
+
translationInfo.set(locale, {
|
|
86
|
+
locale,
|
|
87
|
+
files,
|
|
88
|
+
localeBaseDir: localeBaseDirs[locale],
|
|
89
|
+
});
|
|
64
90
|
}
|
|
65
91
|
}
|
|
66
92
|
}
|
|
@@ -78,17 +104,52 @@ export function vitePlugin(config: EzI18nConfig): Plugin {
|
|
|
78
104
|
? config.locales
|
|
79
105
|
: locales;
|
|
80
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
|
+
|
|
81
136
|
resolved = {
|
|
82
137
|
locales: finalLocales,
|
|
83
138
|
defaultLocale: config.defaultLocale,
|
|
84
139
|
cookieName: config.cookieName ?? 'ez-locale',
|
|
85
140
|
translations,
|
|
141
|
+
pathBasedNamespacing,
|
|
142
|
+
localeBaseDirs,
|
|
86
143
|
};
|
|
87
144
|
|
|
88
145
|
// Build translation info for each locale
|
|
89
146
|
for (const locale of finalLocales) {
|
|
90
147
|
const files = translations[locale] || [];
|
|
91
|
-
const info: TranslationInfo = {
|
|
148
|
+
const info: TranslationInfo = {
|
|
149
|
+
locale,
|
|
150
|
+
files,
|
|
151
|
+
localeBaseDir: localeBaseDirs[locale],
|
|
152
|
+
};
|
|
92
153
|
|
|
93
154
|
// For dev mode, determine if we can use import.meta.glob
|
|
94
155
|
if (isDev && config.translations) {
|
|
@@ -197,8 +258,8 @@ export function t(key, params) {
|
|
|
197
258
|
// ez-i18n:translations - Translation loaders
|
|
198
259
|
if (id === RESOLVED_PREFIX + VIRTUAL_TRANSLATIONS) {
|
|
199
260
|
return isDev
|
|
200
|
-
? generateDevTranslationsModule(translationInfo, viteConfig.root)
|
|
201
|
-
: generateBuildTranslationsModule(translationInfo, viteConfig.root);
|
|
261
|
+
? generateDevTranslationsModule(translationInfo, viteConfig.root, resolved.pathBasedNamespacing)
|
|
262
|
+
: generateBuildTranslationsModule(translationInfo, viteConfig.root, resolved.pathBasedNamespacing);
|
|
202
263
|
}
|
|
203
264
|
|
|
204
265
|
return null;
|
|
@@ -276,7 +337,8 @@ export function t(key, params) {
|
|
|
276
337
|
*/
|
|
277
338
|
function generateDevTranslationsModule(
|
|
278
339
|
translationInfo: Map<string, TranslationInfo>,
|
|
279
|
-
projectRoot: string
|
|
340
|
+
projectRoot: string,
|
|
341
|
+
pathBasedNamespacing: boolean
|
|
280
342
|
): string {
|
|
281
343
|
const imports: string[] = [];
|
|
282
344
|
const loaderEntries: string[] = [];
|
|
@@ -284,12 +346,37 @@ function generateDevTranslationsModule(
|
|
|
284
346
|
// Add deepMerge inline for runtime merging
|
|
285
347
|
imports.push(getDeepMergeCode());
|
|
286
348
|
|
|
349
|
+
// Add namespace wrapper if needed
|
|
350
|
+
if (pathBasedNamespacing) {
|
|
351
|
+
imports.push(generateNamespaceWrapperCode());
|
|
352
|
+
}
|
|
353
|
+
|
|
287
354
|
for (const [locale, info] of translationInfo) {
|
|
288
355
|
if (info.files.length === 0) {
|
|
289
356
|
// No files - return empty object
|
|
290
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
|
+
}`);
|
|
291
378
|
} else if (info.globPattern) {
|
|
292
|
-
// Use import.meta.glob
|
|
379
|
+
// Use import.meta.glob without namespace wrapping
|
|
293
380
|
const varName = `__${locale}Modules`;
|
|
294
381
|
imports.push(
|
|
295
382
|
`const ${varName} = import.meta.glob(${JSON.stringify(info.globPattern)}, { eager: true, import: 'default' });`
|
|
@@ -302,23 +389,45 @@ function generateDevTranslationsModule(
|
|
|
302
389
|
return __deepMerge({}, ...modules);
|
|
303
390
|
}`);
|
|
304
391
|
} else if (info.files.length === 1) {
|
|
305
|
-
// Single file
|
|
392
|
+
// Single file
|
|
306
393
|
const relativePath = toRelativeImport(info.files[0], projectRoot);
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
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
|
+
}
|
|
310
404
|
} else {
|
|
311
405
|
// Multiple explicit files - import all and merge
|
|
312
|
-
|
|
313
|
-
.map(f =>
|
|
314
|
-
|
|
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(', ');
|
|
315
423
|
|
|
316
|
-
|
|
424
|
+
loaderEntries.push(` ${JSON.stringify(locale)}: async () => {
|
|
317
425
|
const modules = await Promise.all([${fileImports}]);
|
|
318
426
|
const contents = modules.map(m => m.default ?? m);
|
|
319
427
|
if (contents.length === 1) return contents[0];
|
|
320
428
|
return __deepMerge({}, ...contents);
|
|
321
429
|
}`);
|
|
430
|
+
}
|
|
322
431
|
}
|
|
323
432
|
}
|
|
324
433
|
|
|
@@ -356,39 +465,69 @@ export async function loadTranslations(locale) {
|
|
|
356
465
|
*/
|
|
357
466
|
function generateBuildTranslationsModule(
|
|
358
467
|
translationInfo: Map<string, TranslationInfo>,
|
|
359
|
-
projectRoot: string
|
|
468
|
+
projectRoot: string,
|
|
469
|
+
pathBasedNamespacing: boolean
|
|
360
470
|
): string {
|
|
361
471
|
const loaderEntries: string[] = [];
|
|
362
472
|
let needsDeepMerge = false;
|
|
473
|
+
let needsNamespaceWrapper = false;
|
|
363
474
|
|
|
364
475
|
for (const [locale, info] of translationInfo) {
|
|
365
476
|
if (info.files.length === 0) {
|
|
366
477
|
loaderEntries.push(` ${JSON.stringify(locale)}: async () => ({})`);
|
|
367
478
|
} else if (info.files.length === 1) {
|
|
368
|
-
// Single file
|
|
479
|
+
// Single file
|
|
369
480
|
const relativePath = toRelativeImport(info.files[0], projectRoot);
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
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
|
+
);
|
|
491
|
+
}
|
|
373
492
|
} else {
|
|
374
493
|
// Multiple files - import and merge
|
|
375
494
|
needsDeepMerge = true;
|
|
376
|
-
const fileImports = info.files
|
|
377
|
-
.map(f => `import(${JSON.stringify(toRelativeImport(f, projectRoot))})`)
|
|
378
|
-
.join(', ');
|
|
379
495
|
|
|
380
|
-
|
|
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 () => {
|
|
381
516
|
const modules = await Promise.all([${fileImports}]);
|
|
382
517
|
const contents = modules.map(m => m.default ?? m);
|
|
383
518
|
return __deepMerge({}, ...contents);
|
|
384
519
|
}`);
|
|
520
|
+
}
|
|
385
521
|
}
|
|
386
522
|
}
|
|
387
523
|
|
|
388
|
-
const
|
|
524
|
+
const helperCode = [
|
|
525
|
+
needsDeepMerge ? getDeepMergeCode() : '',
|
|
526
|
+
needsNamespaceWrapper ? generateNamespaceWrapperCode() : '',
|
|
527
|
+
].filter(Boolean).join('\n');
|
|
389
528
|
|
|
390
529
|
return `
|
|
391
|
-
${
|
|
530
|
+
${helperCode}
|
|
392
531
|
|
|
393
532
|
export const translationLoaders = {
|
|
394
533
|
${loaderEntries.join(',\n')}
|
|
@@ -437,10 +576,13 @@ function __deepMerge(target, ...sources) {
|
|
|
437
576
|
// Re-export resolveConfig for backwards compatibility
|
|
438
577
|
export function resolveConfig(config: EzI18nConfig): ResolvedEzI18nConfig {
|
|
439
578
|
// This is now a simplified version - full resolution happens in buildStart
|
|
579
|
+
const isAutoDiscovery = !config.translations || typeof config.translations === 'string';
|
|
440
580
|
return {
|
|
441
581
|
locales: config.locales || [],
|
|
442
582
|
defaultLocale: config.defaultLocale,
|
|
443
583
|
cookieName: config.cookieName ?? 'ez-locale',
|
|
444
584
|
translations: {},
|
|
585
|
+
pathBasedNamespacing: config.pathBasedNamespacing ?? isAutoDiscovery,
|
|
586
|
+
localeBaseDirs: {},
|
|
445
587
|
};
|
|
446
588
|
}
|