@bleedingdev/modern-js-plugin-i18n 3.2.0-ultramodern.99 → 3.4.0-ultramodern.0

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.
Files changed (98) hide show
  1. package/README.md +221 -11
  2. package/dist/cjs/cli/index.js +17 -64
  3. package/dist/cjs/cli/locales.js +132 -0
  4. package/dist/cjs/runtime/I18nLink.js +17 -20
  5. package/dist/cjs/runtime/Link.js +264 -0
  6. package/dist/cjs/runtime/canonicalRoutes.js +18 -0
  7. package/dist/cjs/runtime/context.js +9 -5
  8. package/dist/cjs/runtime/hooks.js +9 -5
  9. package/dist/cjs/runtime/i18n/backend/config.js +9 -5
  10. package/dist/cjs/runtime/i18n/backend/defaults.js +20 -11
  11. package/dist/cjs/runtime/i18n/backend/defaults.node.js +79 -10
  12. package/dist/cjs/runtime/i18n/backend/index.js +9 -5
  13. package/dist/cjs/runtime/i18n/backend/middleware.common.js +9 -5
  14. package/dist/cjs/runtime/i18n/backend/middleware.js +9 -5
  15. package/dist/cjs/runtime/i18n/backend/middleware.node.js +9 -5
  16. package/dist/cjs/runtime/i18n/backend/sdk-backend.js +9 -5
  17. package/dist/cjs/runtime/i18n/backend/sdk-event.js +16 -11
  18. package/dist/cjs/runtime/i18n/detection/config.js +9 -5
  19. package/dist/cjs/runtime/i18n/detection/index.js +9 -5
  20. package/dist/cjs/runtime/i18n/detection/middleware.js +9 -5
  21. package/dist/cjs/runtime/i18n/detection/middleware.node.js +9 -5
  22. package/dist/cjs/runtime/i18n/index.js +9 -5
  23. package/dist/cjs/runtime/i18n/instance.js +17 -13
  24. package/dist/cjs/runtime/i18n/react-i18next.js +12 -8
  25. package/dist/cjs/runtime/i18n/utils.js +9 -5
  26. package/dist/cjs/runtime/index.js +32 -5
  27. package/dist/cjs/runtime/localizedPaths.js +102 -0
  28. package/dist/cjs/runtime/routerAdapter.js +11 -7
  29. package/dist/cjs/runtime/utils.js +31 -17
  30. package/dist/cjs/server/index.js +10 -14
  31. package/dist/cjs/shared/deepMerge.js +12 -8
  32. package/dist/cjs/shared/detection.js +9 -5
  33. package/dist/cjs/shared/localisedUrls.js +148 -34
  34. package/dist/cjs/shared/utils.js +15 -11
  35. package/dist/esm/cli/index.mjs +8 -48
  36. package/dist/esm/cli/locales.mjs +80 -0
  37. package/dist/esm/runtime/I18nLink.mjs +7 -14
  38. package/dist/esm/runtime/Link.mjs +221 -0
  39. package/dist/esm/runtime/canonicalRoutes.mjs +0 -0
  40. package/dist/esm/runtime/i18n/backend/defaults.mjs +6 -2
  41. package/dist/esm/runtime/i18n/backend/defaults.node.mjs +56 -5
  42. package/dist/esm/runtime/index.mjs +4 -2
  43. package/dist/esm/runtime/localizedPaths.mjs +55 -0
  44. package/dist/esm/runtime/routerAdapter.mjs +3 -3
  45. package/dist/esm/runtime/utils.mjs +19 -12
  46. package/dist/esm/server/index.mjs +2 -10
  47. package/dist/esm/shared/localisedUrls.mjs +115 -23
  48. package/dist/esm-node/cli/index.mjs +8 -48
  49. package/dist/esm-node/cli/locales.mjs +81 -0
  50. package/dist/esm-node/runtime/I18nLink.mjs +7 -14
  51. package/dist/esm-node/runtime/Link.mjs +222 -0
  52. package/dist/esm-node/runtime/canonicalRoutes.mjs +1 -0
  53. package/dist/esm-node/runtime/i18n/backend/defaults.mjs +6 -2
  54. package/dist/esm-node/runtime/i18n/backend/defaults.node.mjs +56 -5
  55. package/dist/esm-node/runtime/index.mjs +4 -2
  56. package/dist/esm-node/runtime/localizedPaths.mjs +56 -0
  57. package/dist/esm-node/runtime/routerAdapter.mjs +3 -3
  58. package/dist/esm-node/runtime/utils.mjs +19 -12
  59. package/dist/esm-node/server/index.mjs +2 -10
  60. package/dist/esm-node/shared/localisedUrls.mjs +115 -23
  61. package/dist/types/cli/index.d.ts +1 -0
  62. package/dist/types/cli/locales.d.ts +17 -0
  63. package/dist/types/runtime/I18nLink.d.ts +4 -13
  64. package/dist/types/runtime/Link.d.ts +66 -0
  65. package/dist/types/runtime/canonicalRoutes.d.ts +60 -0
  66. package/dist/types/runtime/i18n/backend/defaults.d.ts +10 -7
  67. package/dist/types/runtime/i18n/backend/defaults.node.d.ts +13 -4
  68. package/dist/types/runtime/index.d.ts +5 -1
  69. package/dist/types/runtime/localizedPaths.d.ts +39 -0
  70. package/dist/types/runtime/types.d.ts +1 -1
  71. package/dist/types/runtime/utils.d.ts +13 -4
  72. package/dist/types/shared/localisedUrls.d.ts +23 -0
  73. package/dist/types/shared/type.d.ts +27 -5
  74. package/package.json +28 -25
  75. package/rstest.config.mts +7 -2
  76. package/src/cli/index.ts +25 -98
  77. package/src/cli/locales.ts +186 -0
  78. package/src/runtime/I18nLink.tsx +13 -44
  79. package/src/runtime/Link.tsx +430 -0
  80. package/src/runtime/canonicalRoutes.ts +93 -0
  81. package/src/runtime/i18n/backend/defaults.node.ts +112 -7
  82. package/src/runtime/i18n/backend/defaults.ts +20 -18
  83. package/src/runtime/index.tsx +24 -2
  84. package/src/runtime/localizedPaths.ts +107 -0
  85. package/src/runtime/routerAdapter.tsx +4 -5
  86. package/src/runtime/types.ts +1 -1
  87. package/src/runtime/utils.ts +33 -26
  88. package/src/server/index.ts +7 -17
  89. package/src/shared/localisedUrls.ts +256 -26
  90. package/src/shared/type.ts +27 -5
  91. package/tests/backendDefaults.test.ts +51 -0
  92. package/tests/i18nUtils.test.ts +10 -3
  93. package/tests/link.test.tsx +525 -0
  94. package/tests/linkTypes.test.ts +28 -0
  95. package/tests/localisedUrls.test.ts +224 -0
  96. package/tests/routerAdapter.test.tsx +86 -12
  97. package/tests/type-fixture/linkTypes.fixture.tsx +51 -0
  98. package/tests/type-fixture/tsconfig.json +15 -0
@@ -2,12 +2,15 @@ export declare const DEFAULT_I18NEXT_BACKEND_OPTIONS: {
2
2
  loadPath: string;
3
3
  addPath: string;
4
4
  };
5
- declare global {
6
- interface Window {
7
- __assetPrefix__?: string;
8
- }
9
- }
10
- export declare function convertBackendOptions<T extends {
5
+ interface InternalBackendPathOptions {
11
6
  loadPath?: string;
12
7
  addPath?: string;
13
- }>(options: T): T;
8
+ serverLoadPath?: string;
9
+ serverAddPath?: string;
10
+ serverLoadPaths?: string[];
11
+ serverAddPaths?: string[];
12
+ _detectedLoadPath?: string;
13
+ _detectedAddPath?: string;
14
+ }
15
+ export declare function convertBackendOptions<T extends InternalBackendPathOptions>(options: T): T;
16
+ export {};
@@ -1,8 +1,17 @@
1
+ export declare const resolveDefaultLocalesDir: (cwd?: string) => string;
1
2
  export declare const DEFAULT_I18NEXT_BACKEND_OPTIONS: {
2
- loadPath: string;
3
- addPath: string;
3
+ readonly loadPath: string;
4
+ readonly addPath: string;
4
5
  };
5
- export declare function convertBackendOptions<T extends {
6
+ interface InternalBackendPathOptions {
6
7
  loadPath?: string;
7
8
  addPath?: string;
8
- }>(options: T): T;
9
+ serverLoadPath?: string;
10
+ serverAddPath?: string;
11
+ serverLoadPaths?: string[];
12
+ serverAddPaths?: string[];
13
+ _detectedLoadPath?: string;
14
+ _detectedAddPath?: string;
15
+ }
16
+ export declare function convertBackendOptions<T extends InternalBackendPathOptions>(options: T): T;
17
+ export {};
@@ -16,6 +16,10 @@ export interface I18nPluginOptions {
16
16
  [key: string]: any;
17
17
  }
18
18
  export declare const i18nPlugin: (options: I18nPluginOptions) => RuntimePlugin;
19
+ export type { AllowedLinkTarget, CanonicalRoutePath, UltramodernCanonicalRoutes, } from './canonicalRoutes';
19
20
  export { useModernI18n } from './context';
20
- export { I18nLink } from './I18nLink';
21
+ export { I18nLink, type I18nLinkProps } from './I18nLink';
22
+ export { Link, type LinkActiveOptions, type LinkBaseProps, type LinkParams, type LinkProps, } from './Link';
23
+ export { canonicalPath, type LocalizedPathsConfig, localizePath, type UseLocalizedLocationReturn, type UseLocalizedPathsReturn, useLocalizedLocation, useLocalizedPaths, } from './localizedPaths';
24
+ export { buildLocalizedUrl, splitUrlTarget } from './utils';
21
25
  export default i18nPlugin;
@@ -0,0 +1,39 @@
1
+ import type { LocalisedUrlsOption } from '../shared/localisedUrls';
2
+ export interface LocalizedPathsConfig {
3
+ languages: string[];
4
+ localisedUrls?: LocalisedUrlsOption;
5
+ }
6
+ /**
7
+ * Localize a canonical, language-agnostic target for the given language:
8
+ * adds the language prefix and applies `localisedUrls` pattern mapping.
9
+ * `?search`/`#hash` suffixes are preserved verbatim.
10
+ */
11
+ export declare const localizePath: (pathname: string, language: string, config: LocalizedPathsConfig) => string;
12
+ /**
13
+ * Reverse of {@link localizePath}: strip the language prefix and map localized
14
+ * slugs back to the canonical pattern's path. `?search`/`#hash` suffixes are
15
+ * preserved verbatim.
16
+ */
17
+ export declare const canonicalPath: (target: string, config: LocalizedPathsConfig) => string;
18
+ export interface UseLocalizedPathsReturn {
19
+ localizePath: (pathname: string, language: string) => string;
20
+ canonicalPath: (pathname: string) => string;
21
+ }
22
+ /**
23
+ * Context-bound versions of {@link localizePath} and {@link canonicalPath} —
24
+ * the plugin configuration (languages, localisedUrls) is read from the i18n
25
+ * provider, so apps never copy pattern-matching helpers again.
26
+ */
27
+ export declare const useLocalizedPaths: () => UseLocalizedPathsReturn;
28
+ export interface UseLocalizedLocationReturn {
29
+ language: string;
30
+ /** Canonical (language-agnostic) path of the current location. */
31
+ canonical: string;
32
+ /** Per-language hrefs for the current location, search+hash preserved. */
33
+ alternates: Record<string, string>;
34
+ }
35
+ /**
36
+ * Per-language hrefs for the current location — for hreflang `<link>` tags and
37
+ * language switchers. SSR-safe: the location comes from the router adapter.
38
+ */
39
+ export declare const useLocalizedLocation: () => UseLocalizedLocationReturn;
@@ -8,7 +8,7 @@ declare module '@modern-js/runtime' {
8
8
  initOptions?: I18nInitOptions;
9
9
  };
10
10
  }
11
- interface TInternalRuntimeContext {
11
+ interface TRuntimeContext {
12
12
  i18nInstance?: I18nInstance;
13
13
  changeLanguage?: (lang: string) => Promise<void>;
14
14
  }
@@ -1,5 +1,5 @@
1
1
  import { type TInternalRuntimeContext } from '@modern-js/runtime/context';
2
- import type { LocalisedUrlsMap } from '../shared/localisedUrls';
2
+ import type { LocalisedUrlsOption } from '../shared/localisedUrls';
3
3
  export declare const getPathname: (context: TInternalRuntimeContext) => string;
4
4
  export declare const getEntryPath: () => string;
5
5
  /**
@@ -10,14 +10,23 @@ export declare const getEntryPath: () => string;
10
10
  * @returns The detected language or fallback language
11
11
  */
12
12
  export declare const getLanguageFromPath: (pathname: string, languages: string[], fallbackLanguage: string) => string;
13
+ /**
14
+ * Split a link target into its pathname, search and hash parts without
15
+ * relying on `new URL` (SSR-hot path; targets are relative).
16
+ */
17
+ export declare const splitUrlTarget: (target: string) => {
18
+ pathname: string;
19
+ search: string;
20
+ hash: string;
21
+ };
13
22
  /**
14
23
  * Helper function to build localized URL
15
- * @param pathname - The current pathname
24
+ * @param target - The language-agnostic target; may include `?search` and `#hash`
16
25
  * @param language - The target language
17
26
  * @param languages - Array of supported languages
18
- * @returns The localized URL path
27
+ * @returns The localized URL path with search and hash re-appended verbatim
19
28
  */
20
- export declare const buildLocalizedUrl: (pathname: string, language: string, languages: string[], localisedUrls?: boolean | LocalisedUrlsMap) => string;
29
+ export declare const buildLocalizedUrl: (target: string, language: string, languages: string[], localisedUrls?: LocalisedUrlsOption) => string;
21
30
  export declare const detectLanguageFromPath: (pathname: string, languages: string[], localePathRedirect: boolean) => {
22
31
  detected: boolean;
23
32
  language?: string;
@@ -7,7 +7,30 @@ export interface ResolvedLocalisedUrlsConfig {
7
7
  map: LocalisedUrlsMap;
8
8
  }
9
9
  export declare const normalisePathPattern: (path: string) => string;
10
+ /**
11
+ * Normalise a concrete request pathname: slash cleanup only. Unlike
12
+ * {@link normalisePathPattern} it must not rewrite literal `[x]` segments to
13
+ * `:x` params — pathnames are values, not patterns.
14
+ */
15
+ export declare const normalisePathname: (pathname: string) => string;
16
+ /**
17
+ * Localised URLs are strictly opt-in: only an explicit, non-empty map enables
18
+ * route expansion and validation. `true`, `false`, an empty map and absence
19
+ * all resolve to disabled, so upstream-style configs (`localePathRedirect` +
20
+ * `languages` without a map) keep plain locale-prefix behavior instead of
21
+ * failing the build for every route missing from a map they never wrote.
22
+ */
10
23
  export declare const resolveLocalisedUrlsConfig: (option: LocalisedUrlsOption | undefined) => ResolvedLocalisedUrlsConfig;
11
24
  export declare const validateLocalisedUrls: (routes: (NestedRouteForCli | PageRoute)[], languages: string[], localisedUrls: LocalisedUrlsMap) => void;
12
25
  export declare const applyLocalisedUrlsToRoutes: (routes: (NestedRouteForCli | PageRoute)[], languages: string[], localisedUrls: LocalisedUrlsMap) => (NestedRouteForCli | PageRoute)[];
26
+ export declare const matchPathPattern: (pathname: string, pattern: string) => Record<string, string> | null;
27
+ export declare const buildPathFromPattern: (pattern: string, params: Record<string, string>) => string;
13
28
  export declare const resolveLocalisedPath: (pathname: string, targetLanguage: string, languages: string[], localisedUrls: LocalisedUrlsMap) => string;
29
+ /**
30
+ * Reverse-map a language-specific pathname (without language prefix) back to
31
+ * the canonical, language-agnostic path: localized slug patterns are matched
32
+ * against every language variant and rebuilt from the canonical map key.
33
+ */
34
+ export declare const resolveCanonicalLocalisedPath: (pathname: string, languages: string[], localisedUrls: LocalisedUrlsMap) => string;
35
+ export declare const localiseTargetPathname: (pathname: string, language: string, languages: string[], localisedUrls?: LocalisedUrlsOption) => string;
36
+ export declare const canonicalTargetPathname: (pathname: string, languages: string[], localisedUrls?: LocalisedUrlsOption) => string;
@@ -10,12 +10,14 @@ export interface BaseLocaleDetectionOptions {
10
10
  /**
11
11
  * Enables localised pathnames in addition to the locale prefix.
12
12
  *
13
- * - `false`: keep only locale-prefix behavior (`/en/about`).
14
- * - object: map canonical route paths to every configured language.
13
+ * - non-empty object: map canonical route paths to every configured
14
+ * language; route generation then validates that every localisable route
15
+ * path has entries for all configured languages.
16
+ * - absent / `false` / `true` / empty object: keep only locale-prefix
17
+ * behavior (`/en/about`).
15
18
  *
16
- * Defaults to `true` when `localePathRedirect` is enabled, so route
17
- * generation validates that every localisable route path has entries for all
18
- * configured languages.
19
+ * Strictly opt-in: without a map, `localePathRedirect` + `languages` behave
20
+ * exactly like upstream Modern.js.
19
21
  */
20
22
  localisedUrls?: LocalisedUrlsOption;
21
23
  }
@@ -70,6 +72,26 @@ export interface BaseBackendOptions {
70
72
  enabled?: boolean;
71
73
  loadPath?: string;
72
74
  addPath?: string;
75
+ /**
76
+ * Internal file-system path used by the Node.js FS backend.
77
+ * Browser HTTP backend keeps using `loadPath`.
78
+ */
79
+ serverLoadPath?: string;
80
+ /**
81
+ * Internal file-system path candidates used by the Node.js FS backend.
82
+ * The first existing path under current cwd will be used.
83
+ */
84
+ serverLoadPaths?: string[];
85
+ /**
86
+ * Internal file-system path used by the Node.js FS backend.
87
+ * Browser HTTP backend keeps using `addPath`.
88
+ */
89
+ serverAddPath?: string;
90
+ /**
91
+ * Internal file-system path candidates used by the Node.js FS backend.
92
+ * The first existing path under current cwd will be used.
93
+ */
94
+ serverAddPaths?: string[];
73
95
  /**
74
96
  * Cache hit mode for chained backend (only effective when both `loadPath` and `sdk` are provided)
75
97
  *
package/package.json CHANGED
@@ -17,7 +17,7 @@
17
17
  "modern",
18
18
  "modern.js"
19
19
  ],
20
- "version": "3.2.0-ultramodern.99",
20
+ "version": "3.4.0-ultramodern.0",
21
21
  "engines": {
22
22
  "node": ">=20"
23
23
  },
@@ -83,41 +83,44 @@
83
83
  "dependencies": {
84
84
  "@swc/helpers": "^0.5.23",
85
85
  "i18next-browser-languagedetector": "^8.2.1",
86
- "i18next-chained-backend": "^5.0.4",
86
+ "i18next-chained-backend": "^5.0.5",
87
87
  "i18next-fs-backend": "^2.6.6",
88
88
  "i18next-http-backend": "^4.0.0",
89
89
  "i18next-http-middleware": "^3.9.7",
90
- "react-i18next": "17.0.8",
91
- "@modern-js/runtime-utils": "npm:@bleedingdev/modern-js-runtime-utils@3.2.0-ultramodern.99",
92
- "@modern-js/plugin": "npm:@bleedingdev/modern-js-plugin@3.2.0-ultramodern.99",
93
- "@modern-js/types": "npm:@bleedingdev/modern-js-types@3.2.0-ultramodern.99",
94
- "@modern-js/server-core": "npm:@bleedingdev/modern-js-server-core@3.2.0-ultramodern.99",
95
- "@modern-js/utils": "npm:@bleedingdev/modern-js-utils@3.2.0-ultramodern.99",
96
- "@modern-js/server-runtime": "npm:@bleedingdev/modern-js-server-runtime@3.2.0-ultramodern.99"
90
+ "@modern-js/plugin": "npm:@bleedingdev/modern-js-plugin@3.4.0-ultramodern.0",
91
+ "@modern-js/server-runtime": "npm:@bleedingdev/modern-js-server-runtime@3.4.0-ultramodern.0",
92
+ "@modern-js/types": "npm:@bleedingdev/modern-js-types@3.4.0-ultramodern.0",
93
+ "@modern-js/utils": "npm:@bleedingdev/modern-js-utils@3.4.0-ultramodern.0",
94
+ "@modern-js/runtime-utils": "npm:@bleedingdev/modern-js-runtime-utils@3.4.0-ultramodern.0",
95
+ "@modern-js/server-core": "npm:@bleedingdev/modern-js-server-core@3.4.0-ultramodern.0"
97
96
  },
98
97
  "peerDependencies": {
99
- "@modern-js/runtime": "3.2.0-ultramodern.99",
100
- "i18next": ">=25.7.4",
101
- "react": "^19.2.6",
102
- "react-dom": "^19.2.6"
98
+ "@modern-js/runtime": "3.4.0-ultramodern.0",
99
+ "i18next": ">=26.3.1",
100
+ "react": "^19.2.7",
101
+ "react-dom": "^19.2.7",
102
+ "react-i18next": "^17.0.8"
103
103
  },
104
104
  "peerDependenciesMeta": {
105
105
  "i18next": {
106
106
  "optional": true
107
+ },
108
+ "react-i18next": {
109
+ "optional": true
107
110
  }
108
111
  },
109
112
  "devDependencies": {
110
- "@rslib/core": "0.21.5",
111
- "@types/jest": "^30.0.0",
112
- "@types/node": "^25.9.1",
113
- "@typescript/native-preview": "7.0.0-dev.20260527.2",
114
- "i18next": "26.2.0",
115
- "jest": "^30.4.2",
116
- "react": "^19.2.6",
117
- "react-dom": "^19.2.6",
118
- "ts-jest": "^29.4.11",
119
- "@modern-js/app-tools": "npm:@bleedingdev/modern-js-app-tools@3.2.0-ultramodern.99",
120
- "@modern-js/runtime": "npm:@bleedingdev/modern-js-runtime@3.2.0-ultramodern.99"
113
+ "@rslib/core": "0.23.0",
114
+ "@types/node": "^26.0.0",
115
+ "@typescript/native-preview": "7.0.0-dev.20260624.1",
116
+ "i18next": "26.3.2",
117
+ "react": "^19.2.7",
118
+ "react-dom": "^19.2.7",
119
+ "react-i18next": "17.0.8",
120
+ "ts-node": "^10.9.2",
121
+ "typescript": "^6.0.3",
122
+ "@modern-js/app-tools": "npm:@bleedingdev/modern-js-app-tools@3.4.0-ultramodern.0",
123
+ "@modern-js/runtime": "npm:@bleedingdev/modern-js-runtime@3.4.0-ultramodern.0"
121
124
  },
122
125
  "sideEffects": false,
123
126
  "publishConfig": {
@@ -126,7 +129,7 @@
126
129
  },
127
130
  "scripts": {
128
131
  "dev": "rslib build --watch",
129
- "build": "rslib build",
132
+ "build": "rslib build && pnpm -w tsgo:dts \"$PWD\"",
130
133
  "test": "rstest --passWithNoTests"
131
134
  }
132
135
  }
package/rstest.config.mts CHANGED
@@ -26,13 +26,18 @@ export default {
26
26
  withTestPreset({
27
27
  name: 'plugin-i18n-node',
28
28
  testEnvironment: 'node',
29
- include: ['tests/localisedUrls.test.ts'],
29
+ include: [
30
+ 'tests/i18nUtils.test.ts',
31
+ 'tests/localisedUrls.test.ts',
32
+ 'tests/linkTypes.test.ts',
33
+ 'tests/backendDefaults.test.ts',
34
+ ],
30
35
  extends: commonConfig,
31
36
  }),
32
37
  withTestPreset({
33
38
  name: 'plugin-i18n-client',
34
39
  testEnvironment: 'happy-dom',
35
- include: ['tests/routerAdapter.test.tsx'],
40
+ include: ['tests/routerAdapter.test.tsx', 'tests/link.test.tsx'],
36
41
  extends: commonConfig,
37
42
  }),
38
43
  ],
package/src/cli/index.ts CHANGED
@@ -13,81 +13,14 @@ import {
13
13
  } from '../shared/localisedUrls';
14
14
  import type { BackendOptions, LocaleDetectionOptions } from '../shared/type';
15
15
  import { getBackendOptions, getLocaleDetectionOptions } from '../shared/utils';
16
+ import { applyDetectedBackendPaths, detectLocalesDirectory } from './locales';
17
+ import '../runtime/types';
16
18
 
17
19
  export type TransformRuntimeConfigFn = (
18
20
  extendedConfig: Record<string, any>,
19
21
  entrypoint: Entrypoint,
20
22
  ) => Record<string, any>;
21
23
 
22
- /**
23
- * Check if a directory exists and contains JSON files
24
- */
25
- function hasJsonFiles(dirPath: string): boolean {
26
- try {
27
- if (!fs.existsSync(dirPath) || !fs.statSync(dirPath).isDirectory()) {
28
- return false;
29
- }
30
- const entries = fs.readdirSync(dirPath);
31
- // Check if there are any JSON files in the directory or subdirectories
32
- for (const entry of entries) {
33
- const entryPath = path.join(dirPath, entry);
34
- const stat = fs.statSync(entryPath);
35
- if (stat.isFile() && entry.endsWith('.json')) {
36
- return true;
37
- }
38
- if (stat.isDirectory()) {
39
- // Recursively check subdirectories (e.g., locales/en/, locales/zh/)
40
- if (hasJsonFiles(entryPath)) {
41
- return true;
42
- }
43
- }
44
- }
45
- return false;
46
- } catch {
47
- return false;
48
- }
49
- }
50
-
51
- /**
52
- * Auto-detect if locales directory exists with JSON files
53
- * Checks both project root and config/public directory
54
- */
55
- function detectLocalesDirectory(
56
- appDirectory: string,
57
- normalizedConfig?: any,
58
- ): boolean {
59
- // Check project root directory
60
- const rootLocalesPath = path.join(appDirectory, 'locales');
61
- if (hasJsonFiles(rootLocalesPath)) {
62
- return true;
63
- }
64
-
65
- // Check config/public directory
66
- const configPublicPath = path.join(
67
- appDirectory,
68
- 'config',
69
- 'public',
70
- 'locales',
71
- );
72
- if (hasJsonFiles(configPublicPath)) {
73
- return true;
74
- }
75
-
76
- // Check publicDir if configured
77
- const publicDir = normalizedConfig?.server?.publicDir;
78
- if (publicDir) {
79
- const publicDirPath = Array.isArray(publicDir) ? publicDir[0] : publicDir;
80
- const localesPath = path.isAbsolute(publicDirPath)
81
- ? path.join(publicDirPath, 'locales')
82
- : path.join(appDirectory, publicDirPath, 'locales');
83
- if (hasJsonFiles(localesPath)) {
84
- return true;
85
- }
86
- }
87
-
88
- return false;
89
- }
90
-
91
24
  export interface I18nPluginOptions {
92
25
  localeDetection?: LocaleDetectionOptions;
93
26
  backend?: BackendOptions;
@@ -125,10 +58,14 @@ export const i18nPlugin = (
125
58
  // Auto-detect locales directory and enable backend if:
126
59
  // 1. User didn't explicitly set backend.enabled to false
127
60
  // 2. Locales directory exists with JSON files
128
- // 3. If user configured loadPath or addPath, auto-enable backend without detection
61
+ // 3. If user configured loadPath or addPath, auto-enable backend
129
62
  let backendOptions: BackendOptions | undefined;
130
63
  const { appDirectory } = api.getAppContext();
131
64
  const normalizedConfig = api.getNormalizedConfig();
65
+ const detectedLocales = detectLocalesDirectory(
66
+ appDirectory,
67
+ normalizedConfig,
68
+ );
132
69
 
133
70
  if (backend) {
134
71
  const entryBackendOptions = getBackendOptions(
@@ -139,45 +76,35 @@ export const i18nPlugin = (
139
76
  if (entryBackendOptions?.enabled === false) {
140
77
  backendOptions = entryBackendOptions;
141
78
  } else {
142
- // If user configured loadPath or addPath, auto-enable backend
143
- // No need to detect locales directory since user has specified the path
144
- if (entryBackendOptions?.loadPath || entryBackendOptions?.addPath) {
79
+ if (detectedLocales) {
80
+ backendOptions = applyDetectedBackendPaths(
81
+ entryBackendOptions,
82
+ detectedLocales,
83
+ );
84
+ } else if (
85
+ entryBackendOptions?.loadPath ||
86
+ entryBackendOptions?.addPath
87
+ ) {
88
+ // If user configured loadPath or addPath, auto-enable backend even
89
+ // when no local locales directory is detected.
145
90
  backendOptions = {
146
91
  ...entryBackendOptions,
147
92
  enabled: true,
148
93
  };
149
- } else if (entryBackendOptions?.enabled !== true) {
150
- // Auto-detect if enabled is not explicitly true and no loadPath/addPath configured
151
- const hasLocales = detectLocalesDirectory(
152
- appDirectory,
153
- normalizedConfig,
154
- );
155
-
156
- if (hasLocales) {
157
- // Auto-enable backend if locales directory is detected
158
- backendOptions = {
159
- ...entryBackendOptions,
160
- enabled: true,
161
- };
162
- } else {
163
- backendOptions = entryBackendOptions;
164
- }
165
94
  } else {
166
95
  backendOptions = entryBackendOptions;
167
96
  }
168
97
  }
169
98
  } else {
170
99
  // No backend config provided, try auto-detection
171
- const hasLocales = detectLocalesDirectory(
172
- appDirectory,
173
- normalizedConfig,
174
- );
175
-
176
- if (hasLocales) {
100
+ if (detectedLocales) {
177
101
  // Auto-enable backend if locales directory is detected
178
- backendOptions = getBackendOptions(entrypoint.entryName, {
179
- enabled: true,
180
- });
102
+ backendOptions = applyDetectedBackendPaths(
103
+ getBackendOptions(entrypoint.entryName, {
104
+ enabled: true,
105
+ }),
106
+ detectedLocales,
107
+ );
181
108
  }
182
109
  }
183
110