@modern-js/plugin-i18n 3.3.0 → 3.5.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.
@@ -0,0 +1,81 @@
1
+ import "node:module";
2
+ import fs from "fs";
3
+ import path from "path";
4
+ import { getPublicDirRoutePrefixes, normalizePublicDir, normalizePublicDirPath } from "@modern-js/server-core";
5
+ const LOCALES_RESOURCE_PATTERN = '{{lng}}/{{ns}}.json';
6
+ function hasJsonFiles(dirPath) {
7
+ try {
8
+ if (!fs.existsSync(dirPath) || !fs.statSync(dirPath).isDirectory()) return false;
9
+ const entries = fs.readdirSync(dirPath);
10
+ for (const entry of entries){
11
+ const entryPath = path.join(dirPath, entry);
12
+ const stat = fs.statSync(entryPath);
13
+ if (stat.isFile() && entry.endsWith('.json')) return true;
14
+ if (stat.isDirectory() && hasJsonFiles(entryPath)) return true;
15
+ }
16
+ return false;
17
+ } catch {
18
+ return false;
19
+ }
20
+ }
21
+ function toPosixPath(filePath) {
22
+ return filePath.split(path.sep).join(path.posix.sep);
23
+ }
24
+ function getPublicDirOutputPath(publicDir) {
25
+ return normalizePublicDirPath(publicDir);
26
+ }
27
+ function buildDetectedLocalesDirectory(clientBasePath, serverBasePath, serverBasePathCandidates = [
28
+ serverBasePath
29
+ ]) {
30
+ const normalizedClientBasePath = clientBasePath.startsWith('/') ? clientBasePath : `/${clientBasePath}`;
31
+ const normalizedServerBasePath = toPosixPath(serverBasePath).replace(/\/$/, '');
32
+ const normalizedServerBasePathCandidates = Array.from(new Set(serverBasePathCandidates.map((candidate)=>toPosixPath(candidate).replace(/\/$/, ''))));
33
+ const serverLoadPaths = normalizedServerBasePathCandidates.map((candidate)=>`${candidate}/${LOCALES_RESOURCE_PATTERN}`);
34
+ const serverAddPaths = normalizedServerBasePathCandidates.map((candidate)=>`${candidate}/${LOCALES_RESOURCE_PATTERN}`);
35
+ return {
36
+ loadPath: `${normalizedClientBasePath}/${LOCALES_RESOURCE_PATTERN}`,
37
+ addPath: `${normalizedClientBasePath}/${LOCALES_RESOURCE_PATTERN}`,
38
+ serverLoadPath: `${normalizedServerBasePath}/${LOCALES_RESOURCE_PATTERN}`,
39
+ serverAddPath: `${normalizedServerBasePath}/${LOCALES_RESOURCE_PATTERN}`,
40
+ serverLoadPaths,
41
+ serverAddPaths
42
+ };
43
+ }
44
+ function detectLocalesDirectory(appDirectory, normalizedConfig) {
45
+ const publicDirs = normalizePublicDir(normalizedConfig?.server?.publicDir);
46
+ const publicDirPrefixes = getPublicDirRoutePrefixes(normalizedConfig?.server?.publicDir);
47
+ for(let index = 0; index < publicDirs.length; index++){
48
+ const publicDir = publicDirs[index];
49
+ if (path.isAbsolute(publicDir)) continue;
50
+ const publicDirPath = path.join(appDirectory, publicDir);
51
+ const publicDirPrefix = publicDirPrefixes[index] || `/${normalizePublicDirPath(publicDir)}`;
52
+ const publicDirOutputPath = getPublicDirOutputPath(publicDir);
53
+ if ('locales' === path.basename(normalizePublicDirPath(publicDir)) && hasJsonFiles(publicDirPath)) return buildDetectedLocalesDirectory(publicDirPrefix, `./${publicDirOutputPath}`);
54
+ const localesPath = path.join(publicDirPath, 'locales');
55
+ if (hasJsonFiles(localesPath)) return buildDetectedLocalesDirectory(`${publicDirPrefix}/locales`, `./${publicDirOutputPath}/locales`);
56
+ }
57
+ const configPublicPath = path.join(appDirectory, 'config', 'public', 'locales');
58
+ if (hasJsonFiles(configPublicPath)) return buildDetectedLocalesDirectory('/locales', './public/locales', [
59
+ './config/public/locales',
60
+ './public/locales'
61
+ ]);
62
+ const rootLocalesPath = path.join(appDirectory, 'locales');
63
+ if (hasJsonFiles(rootLocalesPath)) return buildDetectedLocalesDirectory('/locales', './locales');
64
+ }
65
+ function applyDetectedBackendPaths(backendOptions, detectedLocales) {
66
+ if (!detectedLocales) return backendOptions;
67
+ const backendWithDetectedPaths = {
68
+ ...backendOptions,
69
+ enabled: true,
70
+ loadPath: backendOptions.loadPath ?? detectedLocales.loadPath,
71
+ addPath: backendOptions.addPath ?? detectedLocales.addPath,
72
+ serverLoadPath: backendOptions.serverLoadPath ?? detectedLocales.serverLoadPath,
73
+ serverLoadPaths: backendOptions.serverLoadPath || backendOptions.serverLoadPaths ? backendOptions.serverLoadPaths : detectedLocales.serverLoadPaths,
74
+ serverAddPath: backendOptions.serverAddPath ?? detectedLocales.serverAddPath,
75
+ serverAddPaths: backendOptions.serverAddPath || backendOptions.serverAddPaths ? backendOptions.serverAddPaths : detectedLocales.serverAddPaths,
76
+ _detectedLoadPath: detectedLocales.loadPath,
77
+ _detectedAddPath: detectedLocales.addPath
78
+ };
79
+ return backendWithDetectedPaths;
80
+ }
81
+ export { applyDetectedBackendPaths, detectLocalesDirectory };
@@ -4,8 +4,6 @@ const DEFAULT_I18NEXT_BACKEND_OPTIONS = {
4
4
  addPath: '/locales/{{lng}}/{{ns}}.json'
5
5
  };
6
6
  function convertPath(path) {
7
- if (!path) return path;
8
- if (path.startsWith('/')) return `${window.__assetPrefix__ || ''}${path}`;
9
7
  return path;
10
8
  }
11
9
  function convertBackendOptions(options) {
@@ -13,6 +11,12 @@ function convertBackendOptions(options) {
13
11
  const converted = {
14
12
  ...options
15
13
  };
14
+ delete converted.serverLoadPath;
15
+ delete converted.serverAddPath;
16
+ delete converted.serverLoadPaths;
17
+ delete converted.serverAddPaths;
18
+ delete converted._detectedLoadPath;
19
+ delete converted._detectedAddPath;
16
20
  if (converted.loadPath) converted.loadPath = convertPath(converted.loadPath);
17
21
  if (converted.addPath) converted.addPath = convertPath(converted.addPath);
18
22
  return converted;
@@ -1,4 +1,5 @@
1
1
  import "node:module";
2
+ import fs from "fs";
2
3
  const DEFAULT_I18NEXT_BACKEND_OPTIONS = {
3
4
  loadPath: './locales/{{lng}}/{{ns}}.json',
4
5
  addPath: './locales/{{lng}}/{{ns}}.json'
@@ -8,13 +9,43 @@ function convertPath(path) {
8
9
  if (path.startsWith('/')) return `.${path}`;
9
10
  return path;
10
11
  }
12
+ function shouldUseServerPath(currentPath, detectedPath) {
13
+ return !detectedPath || currentPath === detectedPath;
14
+ }
15
+ function getResourceBasePath(resourcePath) {
16
+ const markerIndex = resourcePath.indexOf('{{lng}}');
17
+ if (markerIndex < 0) return resourcePath;
18
+ return resourcePath.slice(0, markerIndex).replace(/[\\/]+$/, '');
19
+ }
20
+ function pathExists(resourcePath) {
21
+ try {
22
+ return fs.existsSync(getResourceBasePath(resourcePath));
23
+ } catch {
24
+ return false;
25
+ }
26
+ }
27
+ function getServerPath(pathCandidates, fallbackPath) {
28
+ const candidates = Array.from(new Set([
29
+ ...pathCandidates || [],
30
+ fallbackPath
31
+ ].filter(Boolean)));
32
+ return candidates.find(pathExists) || candidates[0];
33
+ }
11
34
  function convertBackendOptions(options) {
12
35
  if (!options) return options;
13
36
  const converted = {
14
37
  ...options
15
38
  };
16
- if (converted.loadPath) converted.loadPath = convertPath(converted.loadPath);
17
- if (converted.addPath) converted.addPath = convertPath(converted.addPath);
39
+ if ((converted.serverLoadPath || converted.serverLoadPaths) && shouldUseServerPath(converted.loadPath, converted._detectedLoadPath)) converted.loadPath = getServerPath(converted.serverLoadPaths, converted.serverLoadPath);
40
+ else if (converted.loadPath) converted.loadPath = convertPath(converted.loadPath);
41
+ if ((converted.serverAddPath || converted.serverAddPaths) && shouldUseServerPath(converted.addPath, converted._detectedAddPath)) converted.addPath = getServerPath(converted.serverAddPaths, converted.serverAddPath);
42
+ else if (converted.addPath) converted.addPath = convertPath(converted.addPath);
43
+ delete converted.serverLoadPath;
44
+ delete converted.serverAddPath;
45
+ delete converted.serverLoadPaths;
46
+ delete converted.serverAddPaths;
47
+ delete converted._detectedLoadPath;
48
+ delete converted._detectedAddPath;
18
49
  return converted;
19
50
  }
20
51
  export { DEFAULT_I18NEXT_BACKEND_OPTIONS, convertBackendOptions };
@@ -0,0 +1,17 @@
1
+ import type { BackendOptions } from '../shared/type';
2
+ interface NormalizedConfigForLocales {
3
+ server?: {
4
+ publicDir?: string | string[];
5
+ };
6
+ }
7
+ export interface DetectedLocalesDirectory {
8
+ loadPath: string;
9
+ addPath: string;
10
+ serverLoadPath: string;
11
+ serverAddPath: string;
12
+ serverLoadPaths?: string[];
13
+ serverAddPaths?: string[];
14
+ }
15
+ export declare function detectLocalesDirectory(appDirectory: string, normalizedConfig?: NormalizedConfigForLocales): DetectedLocalesDirectory | undefined;
16
+ export declare function applyDetectedBackendPaths(backendOptions: BackendOptions, detectedLocales: DetectedLocalesDirectory | undefined): BackendOptions;
17
+ export {};
@@ -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 {};
@@ -2,7 +2,15 @@ export declare const DEFAULT_I18NEXT_BACKEND_OPTIONS: {
2
2
  loadPath: string;
3
3
  addPath: string;
4
4
  };
5
- export declare function convertBackendOptions<T extends {
5
+ interface InternalBackendPathOptions {
6
6
  loadPath?: string;
7
7
  addPath?: string;
8
- }>(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 {};
@@ -58,6 +58,26 @@ export interface BaseBackendOptions {
58
58
  enabled?: boolean;
59
59
  loadPath?: string;
60
60
  addPath?: string;
61
+ /**
62
+ * Internal file-system path used by the Node.js FS backend.
63
+ * Browser HTTP backend keeps using `loadPath`.
64
+ */
65
+ serverLoadPath?: string;
66
+ /**
67
+ * Internal file-system path candidates used by the Node.js FS backend.
68
+ * The first existing path under current cwd will be used.
69
+ */
70
+ serverLoadPaths?: string[];
71
+ /**
72
+ * Internal file-system path used by the Node.js FS backend.
73
+ * Browser HTTP backend keeps using `addPath`.
74
+ */
75
+ serverAddPath?: string;
76
+ /**
77
+ * Internal file-system path candidates used by the Node.js FS backend.
78
+ * The first existing path under current cwd will be used.
79
+ */
80
+ serverAddPaths?: string[];
61
81
  /**
62
82
  * Cache hit mode for chained backend (only effective when both `loadPath` and `sdk` are provided)
63
83
  *
package/package.json CHANGED
@@ -15,7 +15,7 @@
15
15
  "modern",
16
16
  "modern.js"
17
17
  ],
18
- "version": "3.3.0",
18
+ "version": "3.5.0",
19
19
  "engines": {
20
20
  "node": ">=20"
21
21
  },
@@ -85,19 +85,19 @@
85
85
  "i18next-fs-backend": "^2.6.3",
86
86
  "i18next-http-backend": "^3.0.6",
87
87
  "i18next-http-middleware": "^3.9.7",
88
- "@modern-js/plugin": "3.3.0",
89
- "@modern-js/runtime-utils": "3.3.0",
90
- "@modern-js/server-core": "3.3.0",
91
- "@modern-js/server-runtime": "3.3.0",
92
- "@modern-js/types": "3.3.0",
93
- "@modern-js/utils": "3.3.0"
88
+ "@modern-js/plugin": "3.5.0",
89
+ "@modern-js/runtime-utils": "3.5.0",
90
+ "@modern-js/server-core": "3.5.0",
91
+ "@modern-js/server-runtime": "3.5.0",
92
+ "@modern-js/types": "3.5.0",
93
+ "@modern-js/utils": "3.5.0"
94
94
  },
95
95
  "peerDependencies": {
96
96
  "react": ">=17.0.2",
97
97
  "react-dom": ">=17.0.2",
98
98
  "i18next": ">=25.7.4",
99
99
  "react-i18next": ">=15.7.4",
100
- "@modern-js/runtime": "^3.3.0"
100
+ "@modern-js/runtime": "^3.5.0"
101
101
  },
102
102
  "peerDependenciesMeta": {
103
103
  "i18next": {
@@ -108,7 +108,7 @@
108
108
  }
109
109
  },
110
110
  "devDependencies": {
111
- "@rslib/core": "0.22.0",
111
+ "@rslib/core": "0.23.0",
112
112
  "@types/node": "^20",
113
113
  "i18next": "25.7.4",
114
114
  "react": "^19.2.7",
@@ -116,9 +116,9 @@
116
116
  "react-i18next": "15.7.4",
117
117
  "ts-node": "^10.9.2",
118
118
  "typescript": "^5",
119
+ "@modern-js/app-tools": "3.5.0",
119
120
  "@modern-js/rslib": "2.68.10",
120
- "@modern-js/app-tools": "3.3.0",
121
- "@modern-js/runtime": "3.3.0"
121
+ "@modern-js/runtime": "3.5.0"
122
122
  },
123
123
  "sideEffects": false,
124
124
  "publishConfig": {
package/src/cli/index.ts CHANGED
@@ -1,10 +1,9 @@
1
- import fs from 'fs';
2
- import path from 'path';
3
1
  import type { AppTools, CliPlugin } from '@modern-js/app-tools';
4
2
  import { getPublicDirRoutePrefixes } from '@modern-js/server-core';
5
3
  import type { Entrypoint } from '@modern-js/types';
6
4
  import type { BackendOptions, LocaleDetectionOptions } from '../shared/type';
7
5
  import { getBackendOptions, getLocaleDetectionOptions } from '../shared/utils';
6
+ import { applyDetectedBackendPaths, detectLocalesDirectory } from './locales';
8
7
  import '../runtime/types';
9
8
 
10
9
  export type TransformRuntimeConfigFn = (
@@ -12,75 +11,6 @@ export type TransformRuntimeConfigFn = (
12
11
  entrypoint: Entrypoint,
13
12
  ) => Record<string, any>;
14
13
 
15
- /**
16
- * Check if a directory exists and contains JSON files
17
- */
18
- function hasJsonFiles(dirPath: string): boolean {
19
- try {
20
- if (!fs.existsSync(dirPath) || !fs.statSync(dirPath).isDirectory()) {
21
- return false;
22
- }
23
- const entries = fs.readdirSync(dirPath);
24
- // Check if there are any JSON files in the directory or subdirectories
25
- for (const entry of entries) {
26
- const entryPath = path.join(dirPath, entry);
27
- const stat = fs.statSync(entryPath);
28
- if (stat.isFile() && entry.endsWith('.json')) {
29
- return true;
30
- }
31
- if (stat.isDirectory()) {
32
- // Recursively check subdirectories (e.g., locales/en/, locales/zh/)
33
- if (hasJsonFiles(entryPath)) {
34
- return true;
35
- }
36
- }
37
- }
38
- return false;
39
- } catch {
40
- return false;
41
- }
42
- }
43
-
44
- /**
45
- * Auto-detect if locales directory exists with JSON files
46
- * Checks both project root and config/public directory
47
- */
48
- function detectLocalesDirectory(
49
- appDirectory: string,
50
- normalizedConfig?: any,
51
- ): boolean {
52
- // Check project root directory
53
- const rootLocalesPath = path.join(appDirectory, 'locales');
54
- if (hasJsonFiles(rootLocalesPath)) {
55
- return true;
56
- }
57
-
58
- // Check config/public directory
59
- const configPublicPath = path.join(
60
- appDirectory,
61
- 'config',
62
- 'public',
63
- 'locales',
64
- );
65
- if (hasJsonFiles(configPublicPath)) {
66
- return true;
67
- }
68
-
69
- // Check publicDir if configured
70
- const publicDir = normalizedConfig?.server?.publicDir;
71
- if (publicDir) {
72
- const publicDirPath = Array.isArray(publicDir) ? publicDir[0] : publicDir;
73
- const localesPath = path.isAbsolute(publicDirPath)
74
- ? path.join(publicDirPath, 'locales')
75
- : path.join(appDirectory, publicDirPath, 'locales');
76
- if (hasJsonFiles(localesPath)) {
77
- return true;
78
- }
79
- }
80
-
81
- return false;
82
- }
83
-
84
14
  export interface I18nPluginOptions {
85
15
  localeDetection?: LocaleDetectionOptions;
86
16
  backend?: BackendOptions;
@@ -118,10 +48,14 @@ export const i18nPlugin = (
118
48
  // Auto-detect locales directory and enable backend if:
119
49
  // 1. User didn't explicitly set backend.enabled to false
120
50
  // 2. Locales directory exists with JSON files
121
- // 3. If user configured loadPath or addPath, auto-enable backend without detection
51
+ // 3. If user configured loadPath or addPath, auto-enable backend
122
52
  let backendOptions: BackendOptions | undefined;
123
53
  const { appDirectory } = api.getAppContext();
124
54
  const normalizedConfig = api.getNormalizedConfig();
55
+ const detectedLocales = detectLocalesDirectory(
56
+ appDirectory,
57
+ normalizedConfig,
58
+ );
125
59
 
126
60
  if (backend) {
127
61
  const entryBackendOptions = getBackendOptions(
@@ -132,45 +66,35 @@ export const i18nPlugin = (
132
66
  if (entryBackendOptions?.enabled === false) {
133
67
  backendOptions = entryBackendOptions;
134
68
  } else {
135
- // If user configured loadPath or addPath, auto-enable backend
136
- // No need to detect locales directory since user has specified the path
137
- if (entryBackendOptions?.loadPath || entryBackendOptions?.addPath) {
69
+ if (detectedLocales) {
70
+ backendOptions = applyDetectedBackendPaths(
71
+ entryBackendOptions,
72
+ detectedLocales,
73
+ );
74
+ } else if (
75
+ entryBackendOptions?.loadPath ||
76
+ entryBackendOptions?.addPath
77
+ ) {
78
+ // If user configured loadPath or addPath, auto-enable backend even
79
+ // when no local locales directory is detected.
138
80
  backendOptions = {
139
81
  ...entryBackendOptions,
140
82
  enabled: true,
141
83
  };
142
- } else if (entryBackendOptions?.enabled !== true) {
143
- // Auto-detect if enabled is not explicitly true and no loadPath/addPath configured
144
- const hasLocales = detectLocalesDirectory(
145
- appDirectory,
146
- normalizedConfig,
147
- );
148
-
149
- if (hasLocales) {
150
- // Auto-enable backend if locales directory is detected
151
- backendOptions = {
152
- ...entryBackendOptions,
153
- enabled: true,
154
- };
155
- } else {
156
- backendOptions = entryBackendOptions;
157
- }
158
84
  } else {
159
85
  backendOptions = entryBackendOptions;
160
86
  }
161
87
  }
162
88
  } else {
163
89
  // No backend config provided, try auto-detection
164
- const hasLocales = detectLocalesDirectory(
165
- appDirectory,
166
- normalizedConfig,
167
- );
168
-
169
- if (hasLocales) {
90
+ if (detectedLocales) {
170
91
  // Auto-enable backend if locales directory is detected
171
- backendOptions = getBackendOptions(entrypoint.entryName, {
172
- enabled: true,
173
- });
92
+ backendOptions = applyDetectedBackendPaths(
93
+ getBackendOptions(entrypoint.entryName, {
94
+ enabled: true,
95
+ }),
96
+ detectedLocales,
97
+ );
174
98
  }
175
99
  }
176
100
 
@@ -0,0 +1,186 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import {
4
+ getPublicDirRoutePrefixes,
5
+ normalizePublicDir,
6
+ normalizePublicDirPath,
7
+ } from '@modern-js/server-core';
8
+ import type { BackendOptions } from '../shared/type';
9
+
10
+ interface NormalizedConfigForLocales {
11
+ server?: {
12
+ publicDir?: string | string[];
13
+ };
14
+ }
15
+
16
+ export interface DetectedLocalesDirectory {
17
+ loadPath: string;
18
+ addPath: string;
19
+ serverLoadPath: string;
20
+ serverAddPath: string;
21
+ serverLoadPaths?: string[];
22
+ serverAddPaths?: string[];
23
+ }
24
+
25
+ const LOCALES_RESOURCE_PATTERN = '{{lng}}/{{ns}}.json';
26
+
27
+ function hasJsonFiles(dirPath: string): boolean {
28
+ try {
29
+ if (!fs.existsSync(dirPath) || !fs.statSync(dirPath).isDirectory()) {
30
+ return false;
31
+ }
32
+ const entries = fs.readdirSync(dirPath);
33
+ for (const entry of entries) {
34
+ const entryPath = path.join(dirPath, entry);
35
+ const stat = fs.statSync(entryPath);
36
+ if (stat.isFile() && entry.endsWith('.json')) {
37
+ return true;
38
+ }
39
+ if (stat.isDirectory() && hasJsonFiles(entryPath)) {
40
+ return true;
41
+ }
42
+ }
43
+ return false;
44
+ } catch {
45
+ return false;
46
+ }
47
+ }
48
+
49
+ function toPosixPath(filePath: string): string {
50
+ return filePath.split(path.sep).join(path.posix.sep);
51
+ }
52
+
53
+ function getPublicDirOutputPath(publicDir: string): string {
54
+ return normalizePublicDirPath(publicDir);
55
+ }
56
+
57
+ function buildDetectedLocalesDirectory(
58
+ clientBasePath: string,
59
+ serverBasePath: string,
60
+ serverBasePathCandidates: string[] = [serverBasePath],
61
+ ): DetectedLocalesDirectory {
62
+ const normalizedClientBasePath = clientBasePath.startsWith('/')
63
+ ? clientBasePath
64
+ : `/${clientBasePath}`;
65
+ const normalizedServerBasePath = toPosixPath(serverBasePath).replace(
66
+ /\/$/,
67
+ '',
68
+ );
69
+ const normalizedServerBasePathCandidates = Array.from(
70
+ new Set(
71
+ serverBasePathCandidates.map(candidate =>
72
+ toPosixPath(candidate).replace(/\/$/, ''),
73
+ ),
74
+ ),
75
+ );
76
+ const serverLoadPaths = normalizedServerBasePathCandidates.map(
77
+ candidate => `${candidate}/${LOCALES_RESOURCE_PATTERN}`,
78
+ );
79
+ const serverAddPaths = normalizedServerBasePathCandidates.map(
80
+ candidate => `${candidate}/${LOCALES_RESOURCE_PATTERN}`,
81
+ );
82
+
83
+ return {
84
+ loadPath: `${normalizedClientBasePath}/${LOCALES_RESOURCE_PATTERN}`,
85
+ addPath: `${normalizedClientBasePath}/${LOCALES_RESOURCE_PATTERN}`,
86
+ serverLoadPath: `${normalizedServerBasePath}/${LOCALES_RESOURCE_PATTERN}`,
87
+ serverAddPath: `${normalizedServerBasePath}/${LOCALES_RESOURCE_PATTERN}`,
88
+ serverLoadPaths,
89
+ serverAddPaths,
90
+ };
91
+ }
92
+
93
+ export function detectLocalesDirectory(
94
+ appDirectory: string,
95
+ normalizedConfig?: NormalizedConfigForLocales,
96
+ ): DetectedLocalesDirectory | undefined {
97
+ const publicDirs = normalizePublicDir(normalizedConfig?.server?.publicDir);
98
+ const publicDirPrefixes = getPublicDirRoutePrefixes(
99
+ normalizedConfig?.server?.publicDir,
100
+ );
101
+
102
+ for (let index = 0; index < publicDirs.length; index++) {
103
+ const publicDir = publicDirs[index];
104
+ if (path.isAbsolute(publicDir)) {
105
+ continue;
106
+ }
107
+
108
+ const publicDirPath = path.join(appDirectory, publicDir);
109
+ const publicDirPrefix =
110
+ publicDirPrefixes[index] || `/${normalizePublicDirPath(publicDir)}`;
111
+ const publicDirOutputPath = getPublicDirOutputPath(publicDir);
112
+
113
+ if (
114
+ path.basename(normalizePublicDirPath(publicDir)) === 'locales' &&
115
+ hasJsonFiles(publicDirPath)
116
+ ) {
117
+ return buildDetectedLocalesDirectory(
118
+ publicDirPrefix,
119
+ `./${publicDirOutputPath}`,
120
+ );
121
+ }
122
+
123
+ const localesPath = path.join(publicDirPath, 'locales');
124
+ if (hasJsonFiles(localesPath)) {
125
+ return buildDetectedLocalesDirectory(
126
+ `${publicDirPrefix}/locales`,
127
+ `./${publicDirOutputPath}/locales`,
128
+ );
129
+ }
130
+ }
131
+
132
+ const configPublicPath = path.join(
133
+ appDirectory,
134
+ 'config',
135
+ 'public',
136
+ 'locales',
137
+ );
138
+ if (hasJsonFiles(configPublicPath)) {
139
+ return buildDetectedLocalesDirectory('/locales', './public/locales', [
140
+ './config/public/locales',
141
+ './public/locales',
142
+ ]);
143
+ }
144
+
145
+ const rootLocalesPath = path.join(appDirectory, 'locales');
146
+ if (hasJsonFiles(rootLocalesPath)) {
147
+ return buildDetectedLocalesDirectory('/locales', './locales');
148
+ }
149
+
150
+ return undefined;
151
+ }
152
+
153
+ export function applyDetectedBackendPaths(
154
+ backendOptions: BackendOptions,
155
+ detectedLocales: DetectedLocalesDirectory | undefined,
156
+ ): BackendOptions {
157
+ if (!detectedLocales) {
158
+ return backendOptions;
159
+ }
160
+
161
+ const backendWithDetectedPaths: BackendOptions & {
162
+ _detectedLoadPath: string;
163
+ _detectedAddPath: string;
164
+ } = {
165
+ ...backendOptions,
166
+ enabled: true,
167
+ loadPath: backendOptions.loadPath ?? detectedLocales.loadPath,
168
+ addPath: backendOptions.addPath ?? detectedLocales.addPath,
169
+ serverLoadPath:
170
+ backendOptions.serverLoadPath ?? detectedLocales.serverLoadPath,
171
+ serverLoadPaths:
172
+ backendOptions.serverLoadPath || backendOptions.serverLoadPaths
173
+ ? backendOptions.serverLoadPaths
174
+ : detectedLocales.serverLoadPaths,
175
+ serverAddPath:
176
+ backendOptions.serverAddPath ?? detectedLocales.serverAddPath,
177
+ serverAddPaths:
178
+ backendOptions.serverAddPath || backendOptions.serverAddPaths
179
+ ? backendOptions.serverAddPaths
180
+ : detectedLocales.serverAddPaths,
181
+ _detectedLoadPath: detectedLocales.loadPath,
182
+ _detectedAddPath: detectedLocales.addPath,
183
+ };
184
+
185
+ return backendWithDetectedPaths;
186
+ }