ai-localize-framework-detectors 1.0.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.
- package/dist/index.d.mts +59 -0
- package/dist/index.d.ts +59 -0
- package/dist/index.js +340 -0
- package/dist/index.mjs +301 -0
- package/package.json +34 -0
- package/src/detector.ts +212 -0
- package/src/i18n-detector.ts +140 -0
- package/src/index.ts +2 -0
- package/tsconfig.json +9 -0
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { Framework } from '@ai-localize/shared';
|
|
2
|
+
|
|
3
|
+
interface DetectionResult {
|
|
4
|
+
framework: Framework;
|
|
5
|
+
confidence: number;
|
|
6
|
+
evidence: string[];
|
|
7
|
+
variant?: string;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Detects the frontend framework used in a project directory.
|
|
11
|
+
* Uses package.json dependencies, config files, and folder structure.
|
|
12
|
+
*/
|
|
13
|
+
declare class FrameworkDetector {
|
|
14
|
+
private projectRoot;
|
|
15
|
+
private pkg;
|
|
16
|
+
constructor(projectRoot: string);
|
|
17
|
+
detect(): DetectionResult;
|
|
18
|
+
private allDeps;
|
|
19
|
+
private hasDep;
|
|
20
|
+
private fileExists;
|
|
21
|
+
private detectNextJs;
|
|
22
|
+
private detectReact;
|
|
23
|
+
private detectAngular;
|
|
24
|
+
private detectVue;
|
|
25
|
+
private detectJQuery;
|
|
26
|
+
private detectVanillaJs;
|
|
27
|
+
private detectJsp;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Convenience function to detect framework in a directory
|
|
31
|
+
*/
|
|
32
|
+
declare function detectFramework(projectRoot: string): DetectionResult;
|
|
33
|
+
|
|
34
|
+
interface I18nSetupResult {
|
|
35
|
+
hasI18n: boolean;
|
|
36
|
+
library?: string;
|
|
37
|
+
configFile?: string;
|
|
38
|
+
localesDir?: string;
|
|
39
|
+
namespaces?: string[];
|
|
40
|
+
defaultLanguage?: string;
|
|
41
|
+
targetLanguages?: string[];
|
|
42
|
+
}
|
|
43
|
+
/** Detects existing i18n setup in a project */
|
|
44
|
+
declare class I18nDetector {
|
|
45
|
+
private root;
|
|
46
|
+
private pkg;
|
|
47
|
+
constructor(projectRoot: string);
|
|
48
|
+
detect(framework: Framework): I18nSetupResult;
|
|
49
|
+
private allDeps;
|
|
50
|
+
private fileExists;
|
|
51
|
+
private findLocalesDir;
|
|
52
|
+
private detectLanguagesFromLocalesDir;
|
|
53
|
+
private detectReactI18n;
|
|
54
|
+
private detectAngularI18n;
|
|
55
|
+
private detectVueI18n;
|
|
56
|
+
private detectGenericI18n;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export { type DetectionResult, FrameworkDetector, I18nDetector, type I18nSetupResult, detectFramework };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { Framework } from '@ai-localize/shared';
|
|
2
|
+
|
|
3
|
+
interface DetectionResult {
|
|
4
|
+
framework: Framework;
|
|
5
|
+
confidence: number;
|
|
6
|
+
evidence: string[];
|
|
7
|
+
variant?: string;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Detects the frontend framework used in a project directory.
|
|
11
|
+
* Uses package.json dependencies, config files, and folder structure.
|
|
12
|
+
*/
|
|
13
|
+
declare class FrameworkDetector {
|
|
14
|
+
private projectRoot;
|
|
15
|
+
private pkg;
|
|
16
|
+
constructor(projectRoot: string);
|
|
17
|
+
detect(): DetectionResult;
|
|
18
|
+
private allDeps;
|
|
19
|
+
private hasDep;
|
|
20
|
+
private fileExists;
|
|
21
|
+
private detectNextJs;
|
|
22
|
+
private detectReact;
|
|
23
|
+
private detectAngular;
|
|
24
|
+
private detectVue;
|
|
25
|
+
private detectJQuery;
|
|
26
|
+
private detectVanillaJs;
|
|
27
|
+
private detectJsp;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Convenience function to detect framework in a directory
|
|
31
|
+
*/
|
|
32
|
+
declare function detectFramework(projectRoot: string): DetectionResult;
|
|
33
|
+
|
|
34
|
+
interface I18nSetupResult {
|
|
35
|
+
hasI18n: boolean;
|
|
36
|
+
library?: string;
|
|
37
|
+
configFile?: string;
|
|
38
|
+
localesDir?: string;
|
|
39
|
+
namespaces?: string[];
|
|
40
|
+
defaultLanguage?: string;
|
|
41
|
+
targetLanguages?: string[];
|
|
42
|
+
}
|
|
43
|
+
/** Detects existing i18n setup in a project */
|
|
44
|
+
declare class I18nDetector {
|
|
45
|
+
private root;
|
|
46
|
+
private pkg;
|
|
47
|
+
constructor(projectRoot: string);
|
|
48
|
+
detect(framework: Framework): I18nSetupResult;
|
|
49
|
+
private allDeps;
|
|
50
|
+
private fileExists;
|
|
51
|
+
private findLocalesDir;
|
|
52
|
+
private detectLanguagesFromLocalesDir;
|
|
53
|
+
private detectReactI18n;
|
|
54
|
+
private detectAngularI18n;
|
|
55
|
+
private detectVueI18n;
|
|
56
|
+
private detectGenericI18n;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export { type DetectionResult, FrameworkDetector, I18nDetector, type I18nSetupResult, detectFramework };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var index_exports = {};
|
|
32
|
+
__export(index_exports, {
|
|
33
|
+
FrameworkDetector: () => FrameworkDetector,
|
|
34
|
+
I18nDetector: () => I18nDetector,
|
|
35
|
+
detectFramework: () => detectFramework
|
|
36
|
+
});
|
|
37
|
+
module.exports = __toCommonJS(index_exports);
|
|
38
|
+
|
|
39
|
+
// src/detector.ts
|
|
40
|
+
var fs = __toESM(require("fs"));
|
|
41
|
+
var path = __toESM(require("path"));
|
|
42
|
+
var import_shared = require("@ai-localize/shared");
|
|
43
|
+
var FrameworkDetector = class {
|
|
44
|
+
projectRoot;
|
|
45
|
+
pkg;
|
|
46
|
+
constructor(projectRoot) {
|
|
47
|
+
this.projectRoot = projectRoot;
|
|
48
|
+
this.pkg = (0, import_shared.readJsonSafe)(path.join(projectRoot, "package.json"));
|
|
49
|
+
}
|
|
50
|
+
detect() {
|
|
51
|
+
const results = [
|
|
52
|
+
this.detectNextJs(),
|
|
53
|
+
this.detectReact(),
|
|
54
|
+
this.detectAngular(),
|
|
55
|
+
this.detectVue(),
|
|
56
|
+
this.detectJQuery(),
|
|
57
|
+
this.detectVanillaJs(),
|
|
58
|
+
this.detectJsp()
|
|
59
|
+
];
|
|
60
|
+
results.sort((a, b) => b.confidence - a.confidence);
|
|
61
|
+
const best = results[0];
|
|
62
|
+
if (best.confidence === 0) {
|
|
63
|
+
return { framework: "unknown", confidence: 0, evidence: ["No framework detected"] };
|
|
64
|
+
}
|
|
65
|
+
return best;
|
|
66
|
+
}
|
|
67
|
+
allDeps() {
|
|
68
|
+
if (!this.pkg) return {};
|
|
69
|
+
return {
|
|
70
|
+
...this.pkg.dependencies || {},
|
|
71
|
+
...this.pkg.devDependencies || {},
|
|
72
|
+
...this.pkg.peerDependencies || {}
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
hasDep(name) {
|
|
76
|
+
return name in this.allDeps();
|
|
77
|
+
}
|
|
78
|
+
fileExists(relPath) {
|
|
79
|
+
return fs.existsSync(path.join(this.projectRoot, relPath));
|
|
80
|
+
}
|
|
81
|
+
detectNextJs() {
|
|
82
|
+
const evidence = [];
|
|
83
|
+
let confidence = 0;
|
|
84
|
+
if (this.hasDep("next")) {
|
|
85
|
+
evidence.push("dep: next");
|
|
86
|
+
confidence += 50;
|
|
87
|
+
}
|
|
88
|
+
if (this.fileExists("next.config.js") || this.fileExists("next.config.ts") || this.fileExists("next.config.mjs")) {
|
|
89
|
+
evidence.push("next.config.js found");
|
|
90
|
+
confidence += 40;
|
|
91
|
+
}
|
|
92
|
+
if (this.fileExists("pages") || this.fileExists("app")) {
|
|
93
|
+
evidence.push("pages/app directory found");
|
|
94
|
+
confidence += 10;
|
|
95
|
+
}
|
|
96
|
+
return { framework: "react-nextjs", confidence, evidence };
|
|
97
|
+
}
|
|
98
|
+
detectReact() {
|
|
99
|
+
const evidence = [];
|
|
100
|
+
let confidence = 0;
|
|
101
|
+
let variant = "react";
|
|
102
|
+
if (this.hasDep("react")) {
|
|
103
|
+
evidence.push("dep: react");
|
|
104
|
+
confidence += 40;
|
|
105
|
+
}
|
|
106
|
+
if (this.hasDep("react-dom")) {
|
|
107
|
+
evidence.push("dep: react-dom");
|
|
108
|
+
confidence += 10;
|
|
109
|
+
}
|
|
110
|
+
if (this.hasDep("vite") || this.fileExists("vite.config.ts") || this.fileExists("vite.config.js")) {
|
|
111
|
+
evidence.push("vite config found");
|
|
112
|
+
confidence += 30;
|
|
113
|
+
variant = "react-vite";
|
|
114
|
+
}
|
|
115
|
+
if (this.hasDep("react-scripts")) {
|
|
116
|
+
evidence.push("dep: react-scripts (CRA)");
|
|
117
|
+
confidence += 30;
|
|
118
|
+
variant = "react-cra";
|
|
119
|
+
}
|
|
120
|
+
if (confidence >= 50 && variant === "react") {
|
|
121
|
+
evidence.push("generic react project");
|
|
122
|
+
}
|
|
123
|
+
return { framework: variant, confidence, evidence };
|
|
124
|
+
}
|
|
125
|
+
detectAngular() {
|
|
126
|
+
const evidence = [];
|
|
127
|
+
let confidence = 0;
|
|
128
|
+
let variant = "angular";
|
|
129
|
+
if (this.hasDep("@angular/core")) {
|
|
130
|
+
evidence.push("dep: @angular/core");
|
|
131
|
+
confidence += 50;
|
|
132
|
+
}
|
|
133
|
+
if (this.fileExists("angular.json")) {
|
|
134
|
+
evidence.push("angular.json found");
|
|
135
|
+
confidence += 30;
|
|
136
|
+
}
|
|
137
|
+
if (this.fileExists("tsconfig.app.json")) {
|
|
138
|
+
evidence.push("tsconfig.app.json found");
|
|
139
|
+
confidence += 10;
|
|
140
|
+
}
|
|
141
|
+
if (this.hasDep("@ngx-translate/core")) {
|
|
142
|
+
evidence.push("dep: @ngx-translate/core");
|
|
143
|
+
confidence += 10;
|
|
144
|
+
variant = "angular-ngx";
|
|
145
|
+
} else if (confidence > 0) {
|
|
146
|
+
variant = "angular-i18n";
|
|
147
|
+
}
|
|
148
|
+
return { framework: variant, confidence, evidence };
|
|
149
|
+
}
|
|
150
|
+
detectVue() {
|
|
151
|
+
const evidence = [];
|
|
152
|
+
let confidence = 0;
|
|
153
|
+
let variant = "vue";
|
|
154
|
+
if (this.hasDep("vue")) {
|
|
155
|
+
evidence.push("dep: vue");
|
|
156
|
+
confidence += 50;
|
|
157
|
+
}
|
|
158
|
+
if (this.hasDep("@vue/cli-service") || this.fileExists("vue.config.js")) {
|
|
159
|
+
evidence.push("vue-cli config found");
|
|
160
|
+
confidence += 20;
|
|
161
|
+
}
|
|
162
|
+
if (this.hasDep("vite") && confidence > 0) {
|
|
163
|
+
evidence.push("vite (vue)");
|
|
164
|
+
confidence += 20;
|
|
165
|
+
}
|
|
166
|
+
if (this.hasDep("vue-i18n")) {
|
|
167
|
+
evidence.push("dep: vue-i18n");
|
|
168
|
+
confidence += 10;
|
|
169
|
+
variant = "vue-i18n";
|
|
170
|
+
}
|
|
171
|
+
return { framework: variant, confidence, evidence };
|
|
172
|
+
}
|
|
173
|
+
detectJQuery() {
|
|
174
|
+
const evidence = [];
|
|
175
|
+
let confidence = 0;
|
|
176
|
+
if (this.hasDep("jquery")) {
|
|
177
|
+
evidence.push("dep: jquery");
|
|
178
|
+
confidence += 60;
|
|
179
|
+
}
|
|
180
|
+
const htmlFiles = ["index.html", "public/index.html"];
|
|
181
|
+
for (const f of htmlFiles) {
|
|
182
|
+
if (!this.fileExists(f)) continue;
|
|
183
|
+
try {
|
|
184
|
+
const content = fs.readFileSync(path.join(this.projectRoot, f), "utf-8");
|
|
185
|
+
if (content.includes("jquery") || content.includes("jQuery")) {
|
|
186
|
+
evidence.push(`jQuery reference in ${f}`);
|
|
187
|
+
confidence += 20;
|
|
188
|
+
}
|
|
189
|
+
} catch {
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return { framework: "jquery", confidence, evidence };
|
|
193
|
+
}
|
|
194
|
+
detectVanillaJs() {
|
|
195
|
+
const evidence = [];
|
|
196
|
+
let confidence = 0;
|
|
197
|
+
if (!this.pkg) {
|
|
198
|
+
evidence.push("No package.json");
|
|
199
|
+
confidence = 20;
|
|
200
|
+
} else {
|
|
201
|
+
const deps = Object.keys(this.allDeps());
|
|
202
|
+
if (deps.length === 0) {
|
|
203
|
+
evidence.push("No dependencies");
|
|
204
|
+
confidence = 30;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
if (this.fileExists("index.html")) {
|
|
208
|
+
evidence.push("index.html found");
|
|
209
|
+
confidence += 10;
|
|
210
|
+
}
|
|
211
|
+
return { framework: "vanilla-js", confidence, evidence };
|
|
212
|
+
}
|
|
213
|
+
detectJsp() {
|
|
214
|
+
const evidence = [];
|
|
215
|
+
let confidence = 0;
|
|
216
|
+
if (this.fileExists("WEB-INF") || this.fileExists("src/main/webapp")) {
|
|
217
|
+
evidence.push("WEB-INF/webapp found");
|
|
218
|
+
confidence += 50;
|
|
219
|
+
}
|
|
220
|
+
if (this.fileExists("pom.xml") || this.fileExists("build.gradle")) {
|
|
221
|
+
evidence.push("Java build file found");
|
|
222
|
+
confidence += 30;
|
|
223
|
+
}
|
|
224
|
+
return { framework: "jsp", confidence, evidence };
|
|
225
|
+
}
|
|
226
|
+
};
|
|
227
|
+
function detectFramework(projectRoot) {
|
|
228
|
+
const detector = new FrameworkDetector(projectRoot);
|
|
229
|
+
return detector.detect();
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// src/i18n-detector.ts
|
|
233
|
+
var fs2 = __toESM(require("fs"));
|
|
234
|
+
var path2 = __toESM(require("path"));
|
|
235
|
+
var import_shared2 = require("@ai-localize/shared");
|
|
236
|
+
var I18nDetector = class {
|
|
237
|
+
root;
|
|
238
|
+
pkg;
|
|
239
|
+
constructor(projectRoot) {
|
|
240
|
+
this.root = projectRoot;
|
|
241
|
+
this.pkg = (0, import_shared2.readJsonSafe)(path2.join(projectRoot, "package.json"));
|
|
242
|
+
}
|
|
243
|
+
detect(framework) {
|
|
244
|
+
switch (framework) {
|
|
245
|
+
case "react":
|
|
246
|
+
case "react-cra":
|
|
247
|
+
case "react-vite":
|
|
248
|
+
case "react-nextjs":
|
|
249
|
+
return this.detectReactI18n();
|
|
250
|
+
case "angular":
|
|
251
|
+
case "angular-ngx":
|
|
252
|
+
case "angular-i18n":
|
|
253
|
+
return this.detectAngularI18n();
|
|
254
|
+
case "vue":
|
|
255
|
+
case "vue-i18n":
|
|
256
|
+
return this.detectVueI18n();
|
|
257
|
+
default:
|
|
258
|
+
return this.detectGenericI18n();
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
allDeps() {
|
|
262
|
+
return {
|
|
263
|
+
...this.pkg?.dependencies || {},
|
|
264
|
+
...this.pkg?.devDependencies || {}
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
fileExists(rel) {
|
|
268
|
+
return fs2.existsSync(path2.join(this.root, rel));
|
|
269
|
+
}
|
|
270
|
+
findLocalesDir() {
|
|
271
|
+
const candidates = ["locales", "src/locales", "i18n", "src/i18n", "public/locales"];
|
|
272
|
+
return candidates.find((c) => this.fileExists(c));
|
|
273
|
+
}
|
|
274
|
+
detectLanguagesFromLocalesDir(localesDir) {
|
|
275
|
+
if (!localesDir) return [];
|
|
276
|
+
const full = path2.join(this.root, localesDir);
|
|
277
|
+
try {
|
|
278
|
+
return fs2.readdirSync(full, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
279
|
+
} catch {
|
|
280
|
+
return [];
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
detectReactI18n() {
|
|
284
|
+
const deps = this.allDeps();
|
|
285
|
+
if (!deps["i18next"] && !deps["react-i18next"]) {
|
|
286
|
+
return { hasI18n: false };
|
|
287
|
+
}
|
|
288
|
+
const localesDir = this.findLocalesDir();
|
|
289
|
+
const languages = this.detectLanguagesFromLocalesDir(localesDir);
|
|
290
|
+
const configFile = ["i18n.ts", "i18n.js", "src/i18n.ts", "src/i18n.js"].find(
|
|
291
|
+
(f) => this.fileExists(f)
|
|
292
|
+
);
|
|
293
|
+
return {
|
|
294
|
+
hasI18n: true,
|
|
295
|
+
library: "react-i18next",
|
|
296
|
+
configFile,
|
|
297
|
+
localesDir,
|
|
298
|
+
defaultLanguage: languages[0] || "en",
|
|
299
|
+
targetLanguages: languages.slice(1)
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
detectAngularI18n() {
|
|
303
|
+
const deps = this.allDeps();
|
|
304
|
+
if (!deps["@ngx-translate/core"] && !deps["@angular/localize"]) {
|
|
305
|
+
return { hasI18n: false };
|
|
306
|
+
}
|
|
307
|
+
const localesDir = this.findLocalesDir();
|
|
308
|
+
const library = deps["@ngx-translate/core"] ? "@ngx-translate/core" : "@angular/localize";
|
|
309
|
+
return {
|
|
310
|
+
hasI18n: true,
|
|
311
|
+
library,
|
|
312
|
+
localesDir,
|
|
313
|
+
defaultLanguage: "en"
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
detectVueI18n() {
|
|
317
|
+
const deps = this.allDeps();
|
|
318
|
+
if (!deps["vue-i18n"]) return { hasI18n: false };
|
|
319
|
+
const localesDir = this.findLocalesDir();
|
|
320
|
+
const languages = this.detectLanguagesFromLocalesDir(localesDir);
|
|
321
|
+
return {
|
|
322
|
+
hasI18n: true,
|
|
323
|
+
library: "vue-i18n",
|
|
324
|
+
localesDir,
|
|
325
|
+
defaultLanguage: languages[0] || "en",
|
|
326
|
+
targetLanguages: languages.slice(1)
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
detectGenericI18n() {
|
|
330
|
+
const localesDir = this.findLocalesDir();
|
|
331
|
+
if (!localesDir) return { hasI18n: false };
|
|
332
|
+
return { hasI18n: true, localesDir };
|
|
333
|
+
}
|
|
334
|
+
};
|
|
335
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
336
|
+
0 && (module.exports = {
|
|
337
|
+
FrameworkDetector,
|
|
338
|
+
I18nDetector,
|
|
339
|
+
detectFramework
|
|
340
|
+
});
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
// src/detector.ts
|
|
2
|
+
import * as fs from "fs";
|
|
3
|
+
import * as path from "path";
|
|
4
|
+
import { readJsonSafe } from "@ai-localize/shared";
|
|
5
|
+
var FrameworkDetector = class {
|
|
6
|
+
projectRoot;
|
|
7
|
+
pkg;
|
|
8
|
+
constructor(projectRoot) {
|
|
9
|
+
this.projectRoot = projectRoot;
|
|
10
|
+
this.pkg = readJsonSafe(path.join(projectRoot, "package.json"));
|
|
11
|
+
}
|
|
12
|
+
detect() {
|
|
13
|
+
const results = [
|
|
14
|
+
this.detectNextJs(),
|
|
15
|
+
this.detectReact(),
|
|
16
|
+
this.detectAngular(),
|
|
17
|
+
this.detectVue(),
|
|
18
|
+
this.detectJQuery(),
|
|
19
|
+
this.detectVanillaJs(),
|
|
20
|
+
this.detectJsp()
|
|
21
|
+
];
|
|
22
|
+
results.sort((a, b) => b.confidence - a.confidence);
|
|
23
|
+
const best = results[0];
|
|
24
|
+
if (best.confidence === 0) {
|
|
25
|
+
return { framework: "unknown", confidence: 0, evidence: ["No framework detected"] };
|
|
26
|
+
}
|
|
27
|
+
return best;
|
|
28
|
+
}
|
|
29
|
+
allDeps() {
|
|
30
|
+
if (!this.pkg) return {};
|
|
31
|
+
return {
|
|
32
|
+
...this.pkg.dependencies || {},
|
|
33
|
+
...this.pkg.devDependencies || {},
|
|
34
|
+
...this.pkg.peerDependencies || {}
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
hasDep(name) {
|
|
38
|
+
return name in this.allDeps();
|
|
39
|
+
}
|
|
40
|
+
fileExists(relPath) {
|
|
41
|
+
return fs.existsSync(path.join(this.projectRoot, relPath));
|
|
42
|
+
}
|
|
43
|
+
detectNextJs() {
|
|
44
|
+
const evidence = [];
|
|
45
|
+
let confidence = 0;
|
|
46
|
+
if (this.hasDep("next")) {
|
|
47
|
+
evidence.push("dep: next");
|
|
48
|
+
confidence += 50;
|
|
49
|
+
}
|
|
50
|
+
if (this.fileExists("next.config.js") || this.fileExists("next.config.ts") || this.fileExists("next.config.mjs")) {
|
|
51
|
+
evidence.push("next.config.js found");
|
|
52
|
+
confidence += 40;
|
|
53
|
+
}
|
|
54
|
+
if (this.fileExists("pages") || this.fileExists("app")) {
|
|
55
|
+
evidence.push("pages/app directory found");
|
|
56
|
+
confidence += 10;
|
|
57
|
+
}
|
|
58
|
+
return { framework: "react-nextjs", confidence, evidence };
|
|
59
|
+
}
|
|
60
|
+
detectReact() {
|
|
61
|
+
const evidence = [];
|
|
62
|
+
let confidence = 0;
|
|
63
|
+
let variant = "react";
|
|
64
|
+
if (this.hasDep("react")) {
|
|
65
|
+
evidence.push("dep: react");
|
|
66
|
+
confidence += 40;
|
|
67
|
+
}
|
|
68
|
+
if (this.hasDep("react-dom")) {
|
|
69
|
+
evidence.push("dep: react-dom");
|
|
70
|
+
confidence += 10;
|
|
71
|
+
}
|
|
72
|
+
if (this.hasDep("vite") || this.fileExists("vite.config.ts") || this.fileExists("vite.config.js")) {
|
|
73
|
+
evidence.push("vite config found");
|
|
74
|
+
confidence += 30;
|
|
75
|
+
variant = "react-vite";
|
|
76
|
+
}
|
|
77
|
+
if (this.hasDep("react-scripts")) {
|
|
78
|
+
evidence.push("dep: react-scripts (CRA)");
|
|
79
|
+
confidence += 30;
|
|
80
|
+
variant = "react-cra";
|
|
81
|
+
}
|
|
82
|
+
if (confidence >= 50 && variant === "react") {
|
|
83
|
+
evidence.push("generic react project");
|
|
84
|
+
}
|
|
85
|
+
return { framework: variant, confidence, evidence };
|
|
86
|
+
}
|
|
87
|
+
detectAngular() {
|
|
88
|
+
const evidence = [];
|
|
89
|
+
let confidence = 0;
|
|
90
|
+
let variant = "angular";
|
|
91
|
+
if (this.hasDep("@angular/core")) {
|
|
92
|
+
evidence.push("dep: @angular/core");
|
|
93
|
+
confidence += 50;
|
|
94
|
+
}
|
|
95
|
+
if (this.fileExists("angular.json")) {
|
|
96
|
+
evidence.push("angular.json found");
|
|
97
|
+
confidence += 30;
|
|
98
|
+
}
|
|
99
|
+
if (this.fileExists("tsconfig.app.json")) {
|
|
100
|
+
evidence.push("tsconfig.app.json found");
|
|
101
|
+
confidence += 10;
|
|
102
|
+
}
|
|
103
|
+
if (this.hasDep("@ngx-translate/core")) {
|
|
104
|
+
evidence.push("dep: @ngx-translate/core");
|
|
105
|
+
confidence += 10;
|
|
106
|
+
variant = "angular-ngx";
|
|
107
|
+
} else if (confidence > 0) {
|
|
108
|
+
variant = "angular-i18n";
|
|
109
|
+
}
|
|
110
|
+
return { framework: variant, confidence, evidence };
|
|
111
|
+
}
|
|
112
|
+
detectVue() {
|
|
113
|
+
const evidence = [];
|
|
114
|
+
let confidence = 0;
|
|
115
|
+
let variant = "vue";
|
|
116
|
+
if (this.hasDep("vue")) {
|
|
117
|
+
evidence.push("dep: vue");
|
|
118
|
+
confidence += 50;
|
|
119
|
+
}
|
|
120
|
+
if (this.hasDep("@vue/cli-service") || this.fileExists("vue.config.js")) {
|
|
121
|
+
evidence.push("vue-cli config found");
|
|
122
|
+
confidence += 20;
|
|
123
|
+
}
|
|
124
|
+
if (this.hasDep("vite") && confidence > 0) {
|
|
125
|
+
evidence.push("vite (vue)");
|
|
126
|
+
confidence += 20;
|
|
127
|
+
}
|
|
128
|
+
if (this.hasDep("vue-i18n")) {
|
|
129
|
+
evidence.push("dep: vue-i18n");
|
|
130
|
+
confidence += 10;
|
|
131
|
+
variant = "vue-i18n";
|
|
132
|
+
}
|
|
133
|
+
return { framework: variant, confidence, evidence };
|
|
134
|
+
}
|
|
135
|
+
detectJQuery() {
|
|
136
|
+
const evidence = [];
|
|
137
|
+
let confidence = 0;
|
|
138
|
+
if (this.hasDep("jquery")) {
|
|
139
|
+
evidence.push("dep: jquery");
|
|
140
|
+
confidence += 60;
|
|
141
|
+
}
|
|
142
|
+
const htmlFiles = ["index.html", "public/index.html"];
|
|
143
|
+
for (const f of htmlFiles) {
|
|
144
|
+
if (!this.fileExists(f)) continue;
|
|
145
|
+
try {
|
|
146
|
+
const content = fs.readFileSync(path.join(this.projectRoot, f), "utf-8");
|
|
147
|
+
if (content.includes("jquery") || content.includes("jQuery")) {
|
|
148
|
+
evidence.push(`jQuery reference in ${f}`);
|
|
149
|
+
confidence += 20;
|
|
150
|
+
}
|
|
151
|
+
} catch {
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return { framework: "jquery", confidence, evidence };
|
|
155
|
+
}
|
|
156
|
+
detectVanillaJs() {
|
|
157
|
+
const evidence = [];
|
|
158
|
+
let confidence = 0;
|
|
159
|
+
if (!this.pkg) {
|
|
160
|
+
evidence.push("No package.json");
|
|
161
|
+
confidence = 20;
|
|
162
|
+
} else {
|
|
163
|
+
const deps = Object.keys(this.allDeps());
|
|
164
|
+
if (deps.length === 0) {
|
|
165
|
+
evidence.push("No dependencies");
|
|
166
|
+
confidence = 30;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
if (this.fileExists("index.html")) {
|
|
170
|
+
evidence.push("index.html found");
|
|
171
|
+
confidence += 10;
|
|
172
|
+
}
|
|
173
|
+
return { framework: "vanilla-js", confidence, evidence };
|
|
174
|
+
}
|
|
175
|
+
detectJsp() {
|
|
176
|
+
const evidence = [];
|
|
177
|
+
let confidence = 0;
|
|
178
|
+
if (this.fileExists("WEB-INF") || this.fileExists("src/main/webapp")) {
|
|
179
|
+
evidence.push("WEB-INF/webapp found");
|
|
180
|
+
confidence += 50;
|
|
181
|
+
}
|
|
182
|
+
if (this.fileExists("pom.xml") || this.fileExists("build.gradle")) {
|
|
183
|
+
evidence.push("Java build file found");
|
|
184
|
+
confidence += 30;
|
|
185
|
+
}
|
|
186
|
+
return { framework: "jsp", confidence, evidence };
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
function detectFramework(projectRoot) {
|
|
190
|
+
const detector = new FrameworkDetector(projectRoot);
|
|
191
|
+
return detector.detect();
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// src/i18n-detector.ts
|
|
195
|
+
import * as fs2 from "fs";
|
|
196
|
+
import * as path2 from "path";
|
|
197
|
+
import { readJsonSafe as readJsonSafe2 } from "@ai-localize/shared";
|
|
198
|
+
var I18nDetector = class {
|
|
199
|
+
root;
|
|
200
|
+
pkg;
|
|
201
|
+
constructor(projectRoot) {
|
|
202
|
+
this.root = projectRoot;
|
|
203
|
+
this.pkg = readJsonSafe2(path2.join(projectRoot, "package.json"));
|
|
204
|
+
}
|
|
205
|
+
detect(framework) {
|
|
206
|
+
switch (framework) {
|
|
207
|
+
case "react":
|
|
208
|
+
case "react-cra":
|
|
209
|
+
case "react-vite":
|
|
210
|
+
case "react-nextjs":
|
|
211
|
+
return this.detectReactI18n();
|
|
212
|
+
case "angular":
|
|
213
|
+
case "angular-ngx":
|
|
214
|
+
case "angular-i18n":
|
|
215
|
+
return this.detectAngularI18n();
|
|
216
|
+
case "vue":
|
|
217
|
+
case "vue-i18n":
|
|
218
|
+
return this.detectVueI18n();
|
|
219
|
+
default:
|
|
220
|
+
return this.detectGenericI18n();
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
allDeps() {
|
|
224
|
+
return {
|
|
225
|
+
...this.pkg?.dependencies || {},
|
|
226
|
+
...this.pkg?.devDependencies || {}
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
fileExists(rel) {
|
|
230
|
+
return fs2.existsSync(path2.join(this.root, rel));
|
|
231
|
+
}
|
|
232
|
+
findLocalesDir() {
|
|
233
|
+
const candidates = ["locales", "src/locales", "i18n", "src/i18n", "public/locales"];
|
|
234
|
+
return candidates.find((c) => this.fileExists(c));
|
|
235
|
+
}
|
|
236
|
+
detectLanguagesFromLocalesDir(localesDir) {
|
|
237
|
+
if (!localesDir) return [];
|
|
238
|
+
const full = path2.join(this.root, localesDir);
|
|
239
|
+
try {
|
|
240
|
+
return fs2.readdirSync(full, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
241
|
+
} catch {
|
|
242
|
+
return [];
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
detectReactI18n() {
|
|
246
|
+
const deps = this.allDeps();
|
|
247
|
+
if (!deps["i18next"] && !deps["react-i18next"]) {
|
|
248
|
+
return { hasI18n: false };
|
|
249
|
+
}
|
|
250
|
+
const localesDir = this.findLocalesDir();
|
|
251
|
+
const languages = this.detectLanguagesFromLocalesDir(localesDir);
|
|
252
|
+
const configFile = ["i18n.ts", "i18n.js", "src/i18n.ts", "src/i18n.js"].find(
|
|
253
|
+
(f) => this.fileExists(f)
|
|
254
|
+
);
|
|
255
|
+
return {
|
|
256
|
+
hasI18n: true,
|
|
257
|
+
library: "react-i18next",
|
|
258
|
+
configFile,
|
|
259
|
+
localesDir,
|
|
260
|
+
defaultLanguage: languages[0] || "en",
|
|
261
|
+
targetLanguages: languages.slice(1)
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
detectAngularI18n() {
|
|
265
|
+
const deps = this.allDeps();
|
|
266
|
+
if (!deps["@ngx-translate/core"] && !deps["@angular/localize"]) {
|
|
267
|
+
return { hasI18n: false };
|
|
268
|
+
}
|
|
269
|
+
const localesDir = this.findLocalesDir();
|
|
270
|
+
const library = deps["@ngx-translate/core"] ? "@ngx-translate/core" : "@angular/localize";
|
|
271
|
+
return {
|
|
272
|
+
hasI18n: true,
|
|
273
|
+
library,
|
|
274
|
+
localesDir,
|
|
275
|
+
defaultLanguage: "en"
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
detectVueI18n() {
|
|
279
|
+
const deps = this.allDeps();
|
|
280
|
+
if (!deps["vue-i18n"]) return { hasI18n: false };
|
|
281
|
+
const localesDir = this.findLocalesDir();
|
|
282
|
+
const languages = this.detectLanguagesFromLocalesDir(localesDir);
|
|
283
|
+
return {
|
|
284
|
+
hasI18n: true,
|
|
285
|
+
library: "vue-i18n",
|
|
286
|
+
localesDir,
|
|
287
|
+
defaultLanguage: languages[0] || "en",
|
|
288
|
+
targetLanguages: languages.slice(1)
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
detectGenericI18n() {
|
|
292
|
+
const localesDir = this.findLocalesDir();
|
|
293
|
+
if (!localesDir) return { hasI18n: false };
|
|
294
|
+
return { hasI18n: true, localesDir };
|
|
295
|
+
}
|
|
296
|
+
};
|
|
297
|
+
export {
|
|
298
|
+
FrameworkDetector,
|
|
299
|
+
I18nDetector,
|
|
300
|
+
detectFramework
|
|
301
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ai-localize-framework-detectors",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Auto-detect frontend frameworks from project structure",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"module": "./dist/index.mjs",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.mjs",
|
|
12
|
+
"require": "./dist/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"ai-localize-shared": "1.0.0"
|
|
17
|
+
},
|
|
18
|
+
"devDependencies": {
|
|
19
|
+
"tsup": "^8.0.1",
|
|
20
|
+
"typescript": "^5.3.3",
|
|
21
|
+
"vitest": "^1.2.1"
|
|
22
|
+
},
|
|
23
|
+
"license": "MIT",
|
|
24
|
+
"publishConfig": {
|
|
25
|
+
"access": "public"
|
|
26
|
+
},
|
|
27
|
+
"scripts": {
|
|
28
|
+
"build": "tsup src/index.ts --format cjs,esm --dts",
|
|
29
|
+
"dev": "tsup src/index.ts --format cjs,esm --dts --watch",
|
|
30
|
+
"typecheck": "tsc --noEmit",
|
|
31
|
+
"test": "vitest run",
|
|
32
|
+
"lint": "eslint src --ext .ts"
|
|
33
|
+
}
|
|
34
|
+
}
|
package/src/detector.ts
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
|
|
4
|
+
import type { Framework } from '@ai-localize/shared';
|
|
5
|
+
import { readJsonSafe } from '@ai-localize/shared';
|
|
6
|
+
|
|
7
|
+
export interface DetectionResult {
|
|
8
|
+
framework: Framework;
|
|
9
|
+
confidence: number;
|
|
10
|
+
evidence: string[];
|
|
11
|
+
variant?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface PackageJson {
|
|
15
|
+
dependencies?: Record<string, string>;
|
|
16
|
+
devDependencies?: Record<string, string>;
|
|
17
|
+
peerDependencies?: Record<string, string>;
|
|
18
|
+
scripts?: Record<string, string>;
|
|
19
|
+
name?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Detects the frontend framework used in a project directory.
|
|
24
|
+
* Uses package.json dependencies, config files, and folder structure.
|
|
25
|
+
*/
|
|
26
|
+
export class FrameworkDetector {
|
|
27
|
+
private projectRoot: string;
|
|
28
|
+
private pkg: PackageJson | null;
|
|
29
|
+
|
|
30
|
+
constructor(projectRoot: string) {
|
|
31
|
+
this.projectRoot = projectRoot;
|
|
32
|
+
this.pkg = readJsonSafe<PackageJson>(path.join(projectRoot, 'package.json'));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
detect(): DetectionResult {
|
|
36
|
+
const results: DetectionResult[] = [
|
|
37
|
+
this.detectNextJs(),
|
|
38
|
+
this.detectReact(),
|
|
39
|
+
this.detectAngular(),
|
|
40
|
+
this.detectVue(),
|
|
41
|
+
this.detectJQuery(),
|
|
42
|
+
this.detectVanillaJs(),
|
|
43
|
+
this.detectJsp(),
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
// Sort by confidence descending
|
|
47
|
+
results.sort((a, b) => b.confidence - a.confidence);
|
|
48
|
+
const best = results[0];
|
|
49
|
+
|
|
50
|
+
if (best.confidence === 0) {
|
|
51
|
+
return { framework: 'unknown', confidence: 0, evidence: ['No framework detected'] };
|
|
52
|
+
}
|
|
53
|
+
return best;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
private allDeps(): Record<string, string> {
|
|
57
|
+
if (!this.pkg) return {};
|
|
58
|
+
return {
|
|
59
|
+
...(this.pkg.dependencies || {}),
|
|
60
|
+
...(this.pkg.devDependencies || {}),
|
|
61
|
+
...(this.pkg.peerDependencies || {}),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
private hasDep(name: string): boolean {
|
|
66
|
+
return name in this.allDeps();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
private fileExists(relPath: string): boolean {
|
|
70
|
+
return fs.existsSync(path.join(this.projectRoot, relPath));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
private detectNextJs(): DetectionResult {
|
|
74
|
+
const evidence: string[] = [];
|
|
75
|
+
let confidence = 0;
|
|
76
|
+
|
|
77
|
+
if (this.hasDep('next')) { evidence.push('dep: next'); confidence += 50; }
|
|
78
|
+
if (this.fileExists('next.config.js') || this.fileExists('next.config.ts') || this.fileExists('next.config.mjs')) {
|
|
79
|
+
evidence.push('next.config.js found'); confidence += 40;
|
|
80
|
+
}
|
|
81
|
+
if (this.fileExists('pages') || this.fileExists('app')) {
|
|
82
|
+
evidence.push('pages/app directory found'); confidence += 10;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return { framework: 'react-nextjs', confidence, evidence };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
private detectReact(): DetectionResult {
|
|
89
|
+
const evidence: string[] = [];
|
|
90
|
+
let confidence = 0;
|
|
91
|
+
let variant: Framework = 'react';
|
|
92
|
+
|
|
93
|
+
if (this.hasDep('react')) { evidence.push('dep: react'); confidence += 40; }
|
|
94
|
+
if (this.hasDep('react-dom')) { evidence.push('dep: react-dom'); confidence += 10; }
|
|
95
|
+
|
|
96
|
+
// Vite
|
|
97
|
+
if (this.hasDep('vite') || this.fileExists('vite.config.ts') || this.fileExists('vite.config.js')) {
|
|
98
|
+
evidence.push('vite config found'); confidence += 30; variant = 'react-vite';
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// CRA
|
|
102
|
+
if (this.hasDep('react-scripts')) {
|
|
103
|
+
evidence.push('dep: react-scripts (CRA)'); confidence += 30; variant = 'react-cra';
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// React without specific build tool
|
|
107
|
+
if (confidence >= 50 && variant === 'react') {
|
|
108
|
+
evidence.push('generic react project');
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return { framework: variant, confidence, evidence };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
private detectAngular(): DetectionResult {
|
|
115
|
+
const evidence: string[] = [];
|
|
116
|
+
let confidence = 0;
|
|
117
|
+
let variant: Framework = 'angular';
|
|
118
|
+
|
|
119
|
+
if (this.hasDep('@angular/core')) { evidence.push('dep: @angular/core'); confidence += 50; }
|
|
120
|
+
if (this.fileExists('angular.json')) { evidence.push('angular.json found'); confidence += 30; }
|
|
121
|
+
if (this.fileExists('tsconfig.app.json')) { evidence.push('tsconfig.app.json found'); confidence += 10; }
|
|
122
|
+
|
|
123
|
+
if (this.hasDep('@ngx-translate/core')) {
|
|
124
|
+
evidence.push('dep: @ngx-translate/core'); confidence += 10; variant = 'angular-ngx';
|
|
125
|
+
} else if (confidence > 0) {
|
|
126
|
+
variant = 'angular-i18n';
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return { framework: variant, confidence, evidence };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
private detectVue(): DetectionResult {
|
|
133
|
+
const evidence: string[] = [];
|
|
134
|
+
let confidence = 0;
|
|
135
|
+
let variant: Framework = 'vue';
|
|
136
|
+
|
|
137
|
+
if (this.hasDep('vue')) { evidence.push('dep: vue'); confidence += 50; }
|
|
138
|
+
if (this.hasDep('@vue/cli-service') || this.fileExists('vue.config.js')) {
|
|
139
|
+
evidence.push('vue-cli config found'); confidence += 20;
|
|
140
|
+
}
|
|
141
|
+
if (this.hasDep('vite') && confidence > 0) {
|
|
142
|
+
evidence.push('vite (vue)'); confidence += 20;
|
|
143
|
+
}
|
|
144
|
+
if (this.hasDep('vue-i18n')) {
|
|
145
|
+
evidence.push('dep: vue-i18n'); confidence += 10; variant = 'vue-i18n';
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return { framework: variant, confidence, evidence };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
private detectJQuery(): DetectionResult {
|
|
152
|
+
const evidence: string[] = [];
|
|
153
|
+
let confidence = 0;
|
|
154
|
+
|
|
155
|
+
if (this.hasDep('jquery')) { evidence.push('dep: jquery'); confidence += 60; }
|
|
156
|
+
// Check for jQuery usage in common files
|
|
157
|
+
const htmlFiles = ['index.html', 'public/index.html'];
|
|
158
|
+
for (const f of htmlFiles) {
|
|
159
|
+
if (!this.fileExists(f)) continue;
|
|
160
|
+
try {
|
|
161
|
+
const content = fs.readFileSync(path.join(this.projectRoot, f), 'utf-8');
|
|
162
|
+
if (content.includes('jquery') || content.includes('jQuery')) {
|
|
163
|
+
evidence.push(`jQuery reference in ${f}`); confidence += 20;
|
|
164
|
+
}
|
|
165
|
+
} catch { /* ignore */ }
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return { framework: 'jquery', confidence, evidence };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
private detectVanillaJs(): DetectionResult {
|
|
172
|
+
const evidence: string[] = [];
|
|
173
|
+
let confidence = 0;
|
|
174
|
+
|
|
175
|
+
if (!this.pkg) {
|
|
176
|
+
evidence.push('No package.json'); confidence = 20;
|
|
177
|
+
} else {
|
|
178
|
+
const deps = Object.keys(this.allDeps());
|
|
179
|
+
if (deps.length === 0) { evidence.push('No dependencies'); confidence = 30; }
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Check for plain HTML entry
|
|
183
|
+
if (this.fileExists('index.html')) {
|
|
184
|
+
evidence.push('index.html found'); confidence += 10;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return { framework: 'vanilla-js', confidence, evidence };
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
private detectJsp(): DetectionResult {
|
|
191
|
+
const evidence: string[] = [];
|
|
192
|
+
let confidence = 0;
|
|
193
|
+
|
|
194
|
+
// JSP projects typically have WEB-INF and .jsp files
|
|
195
|
+
if (this.fileExists('WEB-INF') || this.fileExists('src/main/webapp')) {
|
|
196
|
+
evidence.push('WEB-INF/webapp found'); confidence += 50;
|
|
197
|
+
}
|
|
198
|
+
if (this.fileExists('pom.xml') || this.fileExists('build.gradle')) {
|
|
199
|
+
evidence.push('Java build file found'); confidence += 30;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return { framework: 'jsp', confidence, evidence };
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Convenience function to detect framework in a directory
|
|
208
|
+
*/
|
|
209
|
+
export function detectFramework(projectRoot: string): DetectionResult {
|
|
210
|
+
const detector = new FrameworkDetector(projectRoot);
|
|
211
|
+
return detector.detect();
|
|
212
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
|
|
4
|
+
import { readJsonSafe } from '@ai-localize/shared';
|
|
5
|
+
import type { Framework } from '@ai-localize/shared';
|
|
6
|
+
|
|
7
|
+
export interface I18nSetupResult {
|
|
8
|
+
hasI18n: boolean;
|
|
9
|
+
library?: string;
|
|
10
|
+
configFile?: string;
|
|
11
|
+
localesDir?: string;
|
|
12
|
+
namespaces?: string[];
|
|
13
|
+
defaultLanguage?: string;
|
|
14
|
+
targetLanguages?: string[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface PackageJson {
|
|
18
|
+
dependencies?: Record<string, string>;
|
|
19
|
+
devDependencies?: Record<string, string>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Detects existing i18n setup in a project */
|
|
23
|
+
export class I18nDetector {
|
|
24
|
+
private root: string;
|
|
25
|
+
private pkg: PackageJson | null;
|
|
26
|
+
|
|
27
|
+
constructor(projectRoot: string) {
|
|
28
|
+
this.root = projectRoot;
|
|
29
|
+
this.pkg = readJsonSafe<PackageJson>(path.join(projectRoot, 'package.json'));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
detect(framework: Framework): I18nSetupResult {
|
|
33
|
+
switch (framework) {
|
|
34
|
+
case 'react':
|
|
35
|
+
case 'react-cra':
|
|
36
|
+
case 'react-vite':
|
|
37
|
+
case 'react-nextjs':
|
|
38
|
+
return this.detectReactI18n();
|
|
39
|
+
case 'angular':
|
|
40
|
+
case 'angular-ngx':
|
|
41
|
+
case 'angular-i18n':
|
|
42
|
+
return this.detectAngularI18n();
|
|
43
|
+
case 'vue':
|
|
44
|
+
case 'vue-i18n':
|
|
45
|
+
return this.detectVueI18n();
|
|
46
|
+
default:
|
|
47
|
+
return this.detectGenericI18n();
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
private allDeps(): Record<string, string> {
|
|
52
|
+
return {
|
|
53
|
+
...(this.pkg?.dependencies || {}),
|
|
54
|
+
...(this.pkg?.devDependencies || {}),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
private fileExists(rel: string): boolean {
|
|
59
|
+
return fs.existsSync(path.join(this.root, rel));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
private findLocalesDir(): string | undefined {
|
|
63
|
+
const candidates = ['locales', 'src/locales', 'i18n', 'src/i18n', 'public/locales'];
|
|
64
|
+
return candidates.find((c) => this.fileExists(c));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
private detectLanguagesFromLocalesDir(localesDir?: string): string[] {
|
|
68
|
+
if (!localesDir) return [];
|
|
69
|
+
const full = path.join(this.root, localesDir);
|
|
70
|
+
try {
|
|
71
|
+
return fs
|
|
72
|
+
.readdirSync(full, { withFileTypes: true })
|
|
73
|
+
.filter((e) => e.isDirectory())
|
|
74
|
+
.map((e) => e.name);
|
|
75
|
+
} catch {
|
|
76
|
+
return [];
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
private detectReactI18n(): I18nSetupResult {
|
|
81
|
+
const deps = this.allDeps();
|
|
82
|
+
if (!deps['i18next'] && !deps['react-i18next']) {
|
|
83
|
+
return { hasI18n: false };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const localesDir = this.findLocalesDir();
|
|
87
|
+
const languages = this.detectLanguagesFromLocalesDir(localesDir);
|
|
88
|
+
const configFile = ['i18n.ts', 'i18n.js', 'src/i18n.ts', 'src/i18n.js'].find((f) =>
|
|
89
|
+
this.fileExists(f)
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
hasI18n: true,
|
|
94
|
+
library: 'react-i18next',
|
|
95
|
+
configFile,
|
|
96
|
+
localesDir,
|
|
97
|
+
defaultLanguage: languages[0] || 'en',
|
|
98
|
+
targetLanguages: languages.slice(1),
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
private detectAngularI18n(): I18nSetupResult {
|
|
103
|
+
const deps = this.allDeps();
|
|
104
|
+
if (!deps['@ngx-translate/core'] && !deps['@angular/localize']) {
|
|
105
|
+
return { hasI18n: false };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const localesDir = this.findLocalesDir();
|
|
109
|
+
const library = deps['@ngx-translate/core'] ? '@ngx-translate/core' : '@angular/localize';
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
hasI18n: true,
|
|
113
|
+
library,
|
|
114
|
+
localesDir,
|
|
115
|
+
defaultLanguage: 'en',
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
private detectVueI18n(): I18nSetupResult {
|
|
120
|
+
const deps = this.allDeps();
|
|
121
|
+
if (!deps['vue-i18n']) return { hasI18n: false };
|
|
122
|
+
|
|
123
|
+
const localesDir = this.findLocalesDir();
|
|
124
|
+
const languages = this.detectLanguagesFromLocalesDir(localesDir);
|
|
125
|
+
|
|
126
|
+
return {
|
|
127
|
+
hasI18n: true,
|
|
128
|
+
library: 'vue-i18n',
|
|
129
|
+
localesDir,
|
|
130
|
+
defaultLanguage: languages[0] || 'en',
|
|
131
|
+
targetLanguages: languages.slice(1),
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
private detectGenericI18n(): I18nSetupResult {
|
|
136
|
+
const localesDir = this.findLocalesDir();
|
|
137
|
+
if (!localesDir) return { hasI18n: false };
|
|
138
|
+
return { hasI18n: true, localesDir };
|
|
139
|
+
}
|
|
140
|
+
}
|
package/src/index.ts
ADDED