@zachhandley/ez-i18n 0.1.3 → 0.1.6
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 +152 -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 +170 -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,13 +354,14 @@ 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
|
},
|
|
302
361
|
// HMR support for dev mode
|
|
303
362
|
handleHotUpdate({ file, server }) {
|
|
304
363
|
if (!isDev) return;
|
|
364
|
+
if (!file.endsWith(".json")) return;
|
|
305
365
|
for (const info of translationInfo.values()) {
|
|
306
366
|
if (info.files.includes(file)) {
|
|
307
367
|
const mod = server.moduleGraph.getModuleById(RESOLVED_PREFIX + VIRTUAL_TRANSLATIONS);
|
|
@@ -350,13 +410,34 @@ export function t(key, params) {
|
|
|
350
410
|
}
|
|
351
411
|
};
|
|
352
412
|
}
|
|
353
|
-
function generateDevTranslationsModule(translationInfo, projectRoot) {
|
|
413
|
+
function generateDevTranslationsModule(translationInfo, projectRoot, pathBasedNamespacing) {
|
|
354
414
|
const imports = [];
|
|
355
415
|
const loaderEntries = [];
|
|
356
416
|
imports.push(getDeepMergeCode());
|
|
417
|
+
if (pathBasedNamespacing) {
|
|
418
|
+
imports.push(generateNamespaceWrapperCode());
|
|
419
|
+
}
|
|
357
420
|
for (const [locale, info] of translationInfo) {
|
|
358
421
|
if (info.files.length === 0) {
|
|
359
422
|
loaderEntries.push(` ${JSON.stringify(locale)}: async () => ({})`);
|
|
423
|
+
} else if (info.globPattern && pathBasedNamespacing && info.localeBaseDir) {
|
|
424
|
+
const varName = `__${locale}Modules`;
|
|
425
|
+
const localeBaseDir = info.localeBaseDir.replace(/\\/g, "/");
|
|
426
|
+
imports.push(
|
|
427
|
+
`const ${varName} = import.meta.glob(${JSON.stringify(info.globPattern)}, { eager: true, import: 'default' });`
|
|
428
|
+
);
|
|
429
|
+
loaderEntries.push(` ${JSON.stringify(locale)}: async () => {
|
|
430
|
+
const entries = Object.entries(${varName});
|
|
431
|
+
if (entries.length === 0) return {};
|
|
432
|
+
const localeBaseDir = ${JSON.stringify(localeBaseDir)};
|
|
433
|
+
const wrapped = entries.map(([filePath, content]) => {
|
|
434
|
+
// Extract relative path from locale base dir
|
|
435
|
+
const relativePath = filePath.replace(localeBaseDir + '/', '').replace(/\\.json$/i, '');
|
|
436
|
+
const namespace = relativePath.replace(/[\\/]/g, '.').replace(/\\.index$/, '');
|
|
437
|
+
return __wrapWithNamespace(namespace, content);
|
|
438
|
+
});
|
|
439
|
+
return __deepMerge({}, ...wrapped);
|
|
440
|
+
}`);
|
|
360
441
|
} else if (info.globPattern) {
|
|
361
442
|
const varName = `__${locale}Modules`;
|
|
362
443
|
imports.push(
|
|
@@ -370,17 +451,38 @@ function generateDevTranslationsModule(translationInfo, projectRoot) {
|
|
|
370
451
|
}`);
|
|
371
452
|
} else if (info.files.length === 1) {
|
|
372
453
|
const relativePath = toRelativeImport(info.files[0], projectRoot);
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
454
|
+
if (pathBasedNamespacing && info.localeBaseDir) {
|
|
455
|
+
const namespace = getNamespaceFromPath(info.files[0], info.localeBaseDir);
|
|
456
|
+
loaderEntries.push(
|
|
457
|
+
` ${JSON.stringify(locale)}: () => import(${JSON.stringify(relativePath)}).then(m => __wrapWithNamespace(${JSON.stringify(namespace)}, m.default ?? m))`
|
|
458
|
+
);
|
|
459
|
+
} else {
|
|
460
|
+
loaderEntries.push(
|
|
461
|
+
` ${JSON.stringify(locale)}: () => import(${JSON.stringify(relativePath)}).then(m => m.default ?? m)`
|
|
462
|
+
);
|
|
463
|
+
}
|
|
376
464
|
} else {
|
|
377
|
-
|
|
378
|
-
|
|
465
|
+
if (pathBasedNamespacing && info.localeBaseDir) {
|
|
466
|
+
const fileEntries = info.files.map((f) => {
|
|
467
|
+
const relativePath = toRelativeImport(f, projectRoot);
|
|
468
|
+
const namespace = getNamespaceFromPath(f, info.localeBaseDir);
|
|
469
|
+
return `{ path: ${JSON.stringify(relativePath)}, namespace: ${JSON.stringify(namespace)} }`;
|
|
470
|
+
});
|
|
471
|
+
loaderEntries.push(` ${JSON.stringify(locale)}: async () => {
|
|
472
|
+
const fileInfos = [${fileEntries.join(", ")}];
|
|
473
|
+
const modules = await Promise.all(fileInfos.map(f => import(f.path)));
|
|
474
|
+
const wrapped = modules.map((m, i) => __wrapWithNamespace(fileInfos[i].namespace, m.default ?? m));
|
|
475
|
+
return __deepMerge({}, ...wrapped);
|
|
476
|
+
}`);
|
|
477
|
+
} else {
|
|
478
|
+
const fileImports = info.files.map((f) => `import(${JSON.stringify(toRelativeImport(f, projectRoot))})`).join(", ");
|
|
479
|
+
loaderEntries.push(` ${JSON.stringify(locale)}: async () => {
|
|
379
480
|
const modules = await Promise.all([${fileImports}]);
|
|
380
481
|
const contents = modules.map(m => m.default ?? m);
|
|
381
482
|
if (contents.length === 1) return contents[0];
|
|
382
483
|
return __deepMerge({}, ...contents);
|
|
383
484
|
}`);
|
|
485
|
+
}
|
|
384
486
|
}
|
|
385
487
|
}
|
|
386
488
|
return `
|
|
@@ -410,30 +512,57 @@ export async function loadTranslations(locale) {
|
|
|
410
512
|
}
|
|
411
513
|
`;
|
|
412
514
|
}
|
|
413
|
-
function generateBuildTranslationsModule(translationInfo, projectRoot) {
|
|
515
|
+
function generateBuildTranslationsModule(translationInfo, projectRoot, pathBasedNamespacing) {
|
|
414
516
|
const loaderEntries = [];
|
|
415
517
|
let needsDeepMerge = false;
|
|
518
|
+
let needsNamespaceWrapper = false;
|
|
416
519
|
for (const [locale, info] of translationInfo) {
|
|
417
520
|
if (info.files.length === 0) {
|
|
418
521
|
loaderEntries.push(` ${JSON.stringify(locale)}: async () => ({})`);
|
|
419
522
|
} else if (info.files.length === 1) {
|
|
420
523
|
const relativePath = toRelativeImport(info.files[0], projectRoot);
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
524
|
+
if (pathBasedNamespacing && info.localeBaseDir) {
|
|
525
|
+
needsNamespaceWrapper = true;
|
|
526
|
+
const namespace = getNamespaceFromPath(info.files[0], info.localeBaseDir);
|
|
527
|
+
loaderEntries.push(
|
|
528
|
+
` ${JSON.stringify(locale)}: () => import(${JSON.stringify(relativePath)}).then(m => __wrapWithNamespace(${JSON.stringify(namespace)}, m.default ?? m))`
|
|
529
|
+
);
|
|
530
|
+
} else {
|
|
531
|
+
loaderEntries.push(
|
|
532
|
+
` ${JSON.stringify(locale)}: () => import(${JSON.stringify(relativePath)}).then(m => m.default ?? m)`
|
|
533
|
+
);
|
|
534
|
+
}
|
|
424
535
|
} else {
|
|
425
536
|
needsDeepMerge = true;
|
|
426
|
-
|
|
427
|
-
|
|
537
|
+
if (pathBasedNamespacing && info.localeBaseDir) {
|
|
538
|
+
needsNamespaceWrapper = true;
|
|
539
|
+
const fileEntries = info.files.map((f) => {
|
|
540
|
+
const relativePath = toRelativeImport(f, projectRoot);
|
|
541
|
+
const namespace = getNamespaceFromPath(f, info.localeBaseDir);
|
|
542
|
+
return `{ path: ${JSON.stringify(relativePath)}, namespace: ${JSON.stringify(namespace)} }`;
|
|
543
|
+
});
|
|
544
|
+
loaderEntries.push(` ${JSON.stringify(locale)}: async () => {
|
|
545
|
+
const fileInfos = [${fileEntries.join(", ")}];
|
|
546
|
+
const modules = await Promise.all(fileInfos.map(f => import(f.path)));
|
|
547
|
+
const wrapped = modules.map((m, i) => __wrapWithNamespace(fileInfos[i].namespace, m.default ?? m));
|
|
548
|
+
return __deepMerge({}, ...wrapped);
|
|
549
|
+
}`);
|
|
550
|
+
} else {
|
|
551
|
+
const fileImports = info.files.map((f) => `import(${JSON.stringify(toRelativeImport(f, projectRoot))})`).join(", ");
|
|
552
|
+
loaderEntries.push(` ${JSON.stringify(locale)}: async () => {
|
|
428
553
|
const modules = await Promise.all([${fileImports}]);
|
|
429
554
|
const contents = modules.map(m => m.default ?? m);
|
|
430
555
|
return __deepMerge({}, ...contents);
|
|
431
556
|
}`);
|
|
557
|
+
}
|
|
432
558
|
}
|
|
433
559
|
}
|
|
434
|
-
const
|
|
560
|
+
const helperCode = [
|
|
561
|
+
needsDeepMerge ? getDeepMergeCode() : "",
|
|
562
|
+
needsNamespaceWrapper ? generateNamespaceWrapperCode() : ""
|
|
563
|
+
].filter(Boolean).join("\n");
|
|
435
564
|
return `
|
|
436
|
-
${
|
|
565
|
+
${helperCode}
|
|
437
566
|
|
|
438
567
|
export const translationLoaders = {
|
|
439
568
|
${loaderEntries.join(",\n")}
|
|
@@ -475,11 +604,14 @@ function __deepMerge(target, ...sources) {
|
|
|
475
604
|
}`;
|
|
476
605
|
}
|
|
477
606
|
function resolveConfig(config) {
|
|
607
|
+
const isAutoDiscovery = !config.translations || typeof config.translations === "string";
|
|
478
608
|
return {
|
|
479
609
|
locales: config.locales || [],
|
|
480
610
|
defaultLocale: config.defaultLocale,
|
|
481
611
|
cookieName: config.cookieName ?? "ez-locale",
|
|
482
|
-
translations: {}
|
|
612
|
+
translations: {},
|
|
613
|
+
pathBasedNamespacing: config.pathBasedNamespacing ?? isAutoDiscovery,
|
|
614
|
+
localeBaseDirs: {}
|
|
483
615
|
};
|
|
484
616
|
}
|
|
485
617
|
|
|
@@ -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;
|
|
@@ -208,6 +269,9 @@ export function t(key, params) {
|
|
|
208
269
|
handleHotUpdate({ file, server }) {
|
|
209
270
|
if (!isDev) return;
|
|
210
271
|
|
|
272
|
+
// Only process JSON files
|
|
273
|
+
if (!file.endsWith('.json')) return;
|
|
274
|
+
|
|
211
275
|
// Check if the changed file is a translation file
|
|
212
276
|
for (const info of translationInfo.values()) {
|
|
213
277
|
if (info.files.includes(file)) {
|
|
@@ -276,7 +340,8 @@ export function t(key, params) {
|
|
|
276
340
|
*/
|
|
277
341
|
function generateDevTranslationsModule(
|
|
278
342
|
translationInfo: Map<string, TranslationInfo>,
|
|
279
|
-
projectRoot: string
|
|
343
|
+
projectRoot: string,
|
|
344
|
+
pathBasedNamespacing: boolean
|
|
280
345
|
): string {
|
|
281
346
|
const imports: string[] = [];
|
|
282
347
|
const loaderEntries: string[] = [];
|
|
@@ -284,12 +349,37 @@ function generateDevTranslationsModule(
|
|
|
284
349
|
// Add deepMerge inline for runtime merging
|
|
285
350
|
imports.push(getDeepMergeCode());
|
|
286
351
|
|
|
352
|
+
// Add namespace wrapper if needed
|
|
353
|
+
if (pathBasedNamespacing) {
|
|
354
|
+
imports.push(generateNamespaceWrapperCode());
|
|
355
|
+
}
|
|
356
|
+
|
|
287
357
|
for (const [locale, info] of translationInfo) {
|
|
288
358
|
if (info.files.length === 0) {
|
|
289
359
|
// No files - return empty object
|
|
290
360
|
loaderEntries.push(` ${JSON.stringify(locale)}: async () => ({})`);
|
|
361
|
+
} else if (info.globPattern && pathBasedNamespacing && info.localeBaseDir) {
|
|
362
|
+
// Use import.meta.glob with namespace wrapping
|
|
363
|
+
const varName = `__${locale}Modules`;
|
|
364
|
+
const localeBaseDir = info.localeBaseDir.replace(/\\/g, '/');
|
|
365
|
+
imports.push(
|
|
366
|
+
`const ${varName} = import.meta.glob(${JSON.stringify(info.globPattern)}, { eager: true, import: 'default' });`
|
|
367
|
+
);
|
|
368
|
+
|
|
369
|
+
loaderEntries.push(` ${JSON.stringify(locale)}: async () => {
|
|
370
|
+
const entries = Object.entries(${varName});
|
|
371
|
+
if (entries.length === 0) return {};
|
|
372
|
+
const localeBaseDir = ${JSON.stringify(localeBaseDir)};
|
|
373
|
+
const wrapped = entries.map(([filePath, content]) => {
|
|
374
|
+
// Extract relative path from locale base dir
|
|
375
|
+
const relativePath = filePath.replace(localeBaseDir + '/', '').replace(/\\.json$/i, '');
|
|
376
|
+
const namespace = relativePath.replace(/[\\/]/g, '.').replace(/\\.index$/, '');
|
|
377
|
+
return __wrapWithNamespace(namespace, content);
|
|
378
|
+
});
|
|
379
|
+
return __deepMerge({}, ...wrapped);
|
|
380
|
+
}`);
|
|
291
381
|
} else if (info.globPattern) {
|
|
292
|
-
// Use import.meta.glob
|
|
382
|
+
// Use import.meta.glob without namespace wrapping
|
|
293
383
|
const varName = `__${locale}Modules`;
|
|
294
384
|
imports.push(
|
|
295
385
|
`const ${varName} = import.meta.glob(${JSON.stringify(info.globPattern)}, { eager: true, import: 'default' });`
|
|
@@ -302,23 +392,45 @@ function generateDevTranslationsModule(
|
|
|
302
392
|
return __deepMerge({}, ...modules);
|
|
303
393
|
}`);
|
|
304
394
|
} else if (info.files.length === 1) {
|
|
305
|
-
// Single file
|
|
395
|
+
// Single file
|
|
306
396
|
const relativePath = toRelativeImport(info.files[0], projectRoot);
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
397
|
+
if (pathBasedNamespacing && info.localeBaseDir) {
|
|
398
|
+
const namespace = getNamespaceFromPath(info.files[0], info.localeBaseDir);
|
|
399
|
+
loaderEntries.push(
|
|
400
|
+
` ${JSON.stringify(locale)}: () => import(${JSON.stringify(relativePath)}).then(m => __wrapWithNamespace(${JSON.stringify(namespace)}, m.default ?? m))`
|
|
401
|
+
);
|
|
402
|
+
} else {
|
|
403
|
+
loaderEntries.push(
|
|
404
|
+
` ${JSON.stringify(locale)}: () => import(${JSON.stringify(relativePath)}).then(m => m.default ?? m)`
|
|
405
|
+
);
|
|
406
|
+
}
|
|
310
407
|
} else {
|
|
311
408
|
// Multiple explicit files - import all and merge
|
|
312
|
-
|
|
313
|
-
.map(f =>
|
|
314
|
-
|
|
409
|
+
if (pathBasedNamespacing && info.localeBaseDir) {
|
|
410
|
+
const fileEntries = info.files.map(f => {
|
|
411
|
+
const relativePath = toRelativeImport(f, projectRoot);
|
|
412
|
+
const namespace = getNamespaceFromPath(f, info.localeBaseDir!);
|
|
413
|
+
return `{ path: ${JSON.stringify(relativePath)}, namespace: ${JSON.stringify(namespace)} }`;
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
loaderEntries.push(` ${JSON.stringify(locale)}: async () => {
|
|
417
|
+
const fileInfos = [${fileEntries.join(', ')}];
|
|
418
|
+
const modules = await Promise.all(fileInfos.map(f => import(f.path)));
|
|
419
|
+
const wrapped = modules.map((m, i) => __wrapWithNamespace(fileInfos[i].namespace, m.default ?? m));
|
|
420
|
+
return __deepMerge({}, ...wrapped);
|
|
421
|
+
}`);
|
|
422
|
+
} else {
|
|
423
|
+
const fileImports = info.files
|
|
424
|
+
.map(f => `import(${JSON.stringify(toRelativeImport(f, projectRoot))})`)
|
|
425
|
+
.join(', ');
|
|
315
426
|
|
|
316
|
-
|
|
427
|
+
loaderEntries.push(` ${JSON.stringify(locale)}: async () => {
|
|
317
428
|
const modules = await Promise.all([${fileImports}]);
|
|
318
429
|
const contents = modules.map(m => m.default ?? m);
|
|
319
430
|
if (contents.length === 1) return contents[0];
|
|
320
431
|
return __deepMerge({}, ...contents);
|
|
321
432
|
}`);
|
|
433
|
+
}
|
|
322
434
|
}
|
|
323
435
|
}
|
|
324
436
|
|
|
@@ -356,39 +468,69 @@ export async function loadTranslations(locale) {
|
|
|
356
468
|
*/
|
|
357
469
|
function generateBuildTranslationsModule(
|
|
358
470
|
translationInfo: Map<string, TranslationInfo>,
|
|
359
|
-
projectRoot: string
|
|
471
|
+
projectRoot: string,
|
|
472
|
+
pathBasedNamespacing: boolean
|
|
360
473
|
): string {
|
|
361
474
|
const loaderEntries: string[] = [];
|
|
362
475
|
let needsDeepMerge = false;
|
|
476
|
+
let needsNamespaceWrapper = false;
|
|
363
477
|
|
|
364
478
|
for (const [locale, info] of translationInfo) {
|
|
365
479
|
if (info.files.length === 0) {
|
|
366
480
|
loaderEntries.push(` ${JSON.stringify(locale)}: async () => ({})`);
|
|
367
481
|
} else if (info.files.length === 1) {
|
|
368
|
-
// Single file
|
|
482
|
+
// Single file
|
|
369
483
|
const relativePath = toRelativeImport(info.files[0], projectRoot);
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
484
|
+
if (pathBasedNamespacing && info.localeBaseDir) {
|
|
485
|
+
needsNamespaceWrapper = true;
|
|
486
|
+
const namespace = getNamespaceFromPath(info.files[0], info.localeBaseDir);
|
|
487
|
+
loaderEntries.push(
|
|
488
|
+
` ${JSON.stringify(locale)}: () => import(${JSON.stringify(relativePath)}).then(m => __wrapWithNamespace(${JSON.stringify(namespace)}, m.default ?? m))`
|
|
489
|
+
);
|
|
490
|
+
} else {
|
|
491
|
+
loaderEntries.push(
|
|
492
|
+
` ${JSON.stringify(locale)}: () => import(${JSON.stringify(relativePath)}).then(m => m.default ?? m)`
|
|
493
|
+
);
|
|
494
|
+
}
|
|
373
495
|
} else {
|
|
374
496
|
// Multiple files - import and merge
|
|
375
497
|
needsDeepMerge = true;
|
|
376
|
-
const fileImports = info.files
|
|
377
|
-
.map(f => `import(${JSON.stringify(toRelativeImport(f, projectRoot))})`)
|
|
378
|
-
.join(', ');
|
|
379
498
|
|
|
380
|
-
|
|
499
|
+
if (pathBasedNamespacing && info.localeBaseDir) {
|
|
500
|
+
needsNamespaceWrapper = true;
|
|
501
|
+
const fileEntries = info.files.map(f => {
|
|
502
|
+
const relativePath = toRelativeImport(f, projectRoot);
|
|
503
|
+
const namespace = getNamespaceFromPath(f, info.localeBaseDir!);
|
|
504
|
+
return `{ path: ${JSON.stringify(relativePath)}, namespace: ${JSON.stringify(namespace)} }`;
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
loaderEntries.push(` ${JSON.stringify(locale)}: async () => {
|
|
508
|
+
const fileInfos = [${fileEntries.join(', ')}];
|
|
509
|
+
const modules = await Promise.all(fileInfos.map(f => import(f.path)));
|
|
510
|
+
const wrapped = modules.map((m, i) => __wrapWithNamespace(fileInfos[i].namespace, m.default ?? m));
|
|
511
|
+
return __deepMerge({}, ...wrapped);
|
|
512
|
+
}`);
|
|
513
|
+
} else {
|
|
514
|
+
const fileImports = info.files
|
|
515
|
+
.map(f => `import(${JSON.stringify(toRelativeImport(f, projectRoot))})`)
|
|
516
|
+
.join(', ');
|
|
517
|
+
|
|
518
|
+
loaderEntries.push(` ${JSON.stringify(locale)}: async () => {
|
|
381
519
|
const modules = await Promise.all([${fileImports}]);
|
|
382
520
|
const contents = modules.map(m => m.default ?? m);
|
|
383
521
|
return __deepMerge({}, ...contents);
|
|
384
522
|
}`);
|
|
523
|
+
}
|
|
385
524
|
}
|
|
386
525
|
}
|
|
387
526
|
|
|
388
|
-
const
|
|
527
|
+
const helperCode = [
|
|
528
|
+
needsDeepMerge ? getDeepMergeCode() : '',
|
|
529
|
+
needsNamespaceWrapper ? generateNamespaceWrapperCode() : '',
|
|
530
|
+
].filter(Boolean).join('\n');
|
|
389
531
|
|
|
390
532
|
return `
|
|
391
|
-
${
|
|
533
|
+
${helperCode}
|
|
392
534
|
|
|
393
535
|
export const translationLoaders = {
|
|
394
536
|
${loaderEntries.join(',\n')}
|
|
@@ -437,10 +579,13 @@ function __deepMerge(target, ...sources) {
|
|
|
437
579
|
// Re-export resolveConfig for backwards compatibility
|
|
438
580
|
export function resolveConfig(config: EzI18nConfig): ResolvedEzI18nConfig {
|
|
439
581
|
// This is now a simplified version - full resolution happens in buildStart
|
|
582
|
+
const isAutoDiscovery = !config.translations || typeof config.translations === 'string';
|
|
440
583
|
return {
|
|
441
584
|
locales: config.locales || [],
|
|
442
585
|
defaultLocale: config.defaultLocale,
|
|
443
586
|
cookieName: config.cookieName ?? 'ez-locale',
|
|
444
587
|
translations: {},
|
|
588
|
+
pathBasedNamespacing: config.pathBasedNamespacing ?? isAutoDiscovery,
|
|
589
|
+
localeBaseDirs: {},
|
|
445
590
|
};
|
|
446
591
|
}
|