translint 0.2.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.js ADDED
@@ -0,0 +1,678 @@
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
+ analyzeProject: () => analyzeProject,
34
+ computeScore: () => computeScore,
35
+ loadConfig: () => loadConfig
36
+ });
37
+ module.exports = __toCommonJS(index_exports);
38
+
39
+ // src/core/analyze.ts
40
+ var import_fs2 = __toESM(require("fs"));
41
+ var import_path2 = __toESM(require("path"));
42
+
43
+ // src/core/localeDiscovery.ts
44
+ var import_path = __toESM(require("path"));
45
+ var import_fs = __toESM(require("fs"));
46
+
47
+ // src/utils/flatten.ts
48
+ function flattenJson(value, prefix = "", out = /* @__PURE__ */ new Map()) {
49
+ for (const [key, nested] of Object.entries(value)) {
50
+ const nextKey = prefix ? `${prefix}.${key}` : key;
51
+ if (nested && typeof nested === "object" && !Array.isArray(nested)) {
52
+ flattenJson(nested, nextKey, out);
53
+ } else {
54
+ out.set(nextKey, nested);
55
+ }
56
+ }
57
+ return out;
58
+ }
59
+
60
+ // src/core/localeDiscovery.ts
61
+ function discoverLocales(config) {
62
+ const dir = import_path.default.resolve(config.projectRoot, config.locales.dir);
63
+ const extension = config.locales.extension || ".json";
64
+ const fileName = config.locales.fileName || "trad";
65
+ if (!import_fs.default.existsSync(dir)) {
66
+ throw new Error(`Locales directory not found: ${dir}`);
67
+ }
68
+ const entries = import_fs.default.readdirSync(dir, { withFileTypes: true });
69
+ const localeFiles = entries.filter((entry) => entry.isFile() && entry.name.endsWith(extension)).map((entry) => ({
70
+ locale: import_path.default.basename(entry.name, extension),
71
+ path: import_path.default.join(dir, entry.name)
72
+ }));
73
+ if (localeFiles.length > 0) {
74
+ return localeFiles.map((file) => {
75
+ const content = JSON.parse(import_fs.default.readFileSync(file.path, "utf8"));
76
+ return {
77
+ locale: file.locale,
78
+ path: file.path,
79
+ content,
80
+ flat: flattenJson(content)
81
+ };
82
+ });
83
+ }
84
+ const localeFolders = entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name);
85
+ return localeFolders.map((locale) => {
86
+ const localePath = import_path.default.join(dir, locale, `${fileName}${extension}`);
87
+ if (!import_fs.default.existsSync(localePath)) {
88
+ return null;
89
+ }
90
+ const content = JSON.parse(import_fs.default.readFileSync(localePath, "utf8"));
91
+ return {
92
+ locale,
93
+ path: localePath,
94
+ content,
95
+ flat: flattenJson(content)
96
+ };
97
+ }).filter((entry) => entry !== null);
98
+ }
99
+
100
+ // src/core/sourceDiscovery.ts
101
+ var import_fast_glob = __toESM(require("fast-glob"));
102
+ async function discoverSourceFiles(config) {
103
+ const sources = config.source.length ? config.source : ["src"];
104
+ const patterns = sources.map((entry) => buildGlob(entry));
105
+ const entries = await (0, import_fast_glob.default)(patterns, {
106
+ cwd: config.projectRoot,
107
+ ignore: config.ignore.map((value) => toPosix(value)),
108
+ absolute: true
109
+ });
110
+ return entries;
111
+ }
112
+ function buildGlob(entry) {
113
+ const normalized = toPosix(entry);
114
+ if (normalized.includes("*")) {
115
+ return normalized;
116
+ }
117
+ const trimmed = normalized.endsWith("/") ? normalized.slice(0, -1) : normalized;
118
+ return `${trimmed}/**/*.{ts,tsx,js,jsx,vue,svelte,html}`;
119
+ }
120
+ function toPosix(value) {
121
+ return value.replace(/\\/g, "/");
122
+ }
123
+
124
+ // src/core/codeUsage.ts
125
+ var import_typescript = __toESM(require("typescript"));
126
+ var SUPPORTED_EXTENSIONS = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx"]);
127
+ function extractCodeUsages(filePath, content, config) {
128
+ const extension = filePath.slice(filePath.lastIndexOf("."));
129
+ if (!SUPPORTED_EXTENSIONS.has(extension)) {
130
+ return [];
131
+ }
132
+ const sourceFile = import_typescript.default.createSourceFile(
133
+ filePath,
134
+ content,
135
+ import_typescript.default.ScriptTarget.ES2020,
136
+ true,
137
+ extension === ".tsx" || extension === ".jsx" ? import_typescript.default.ScriptKind.TSX : import_typescript.default.ScriptKind.TS
138
+ );
139
+ const usages = [];
140
+ const functions = new Set(config.patterns.functions);
141
+ const visit = (node) => {
142
+ if (import_typescript.default.isCallExpression(node)) {
143
+ const calleeName = getCalleeName(node.expression);
144
+ if (calleeName && functions.has(calleeName)) {
145
+ const arg = node.arguments[0];
146
+ if (arg) {
147
+ const literal = getLiteralText(arg);
148
+ const { line, character } = sourceFile.getLineAndCharacterOfPosition(arg.getStart());
149
+ usages.push({
150
+ key: literal ?? "",
151
+ file: filePath,
152
+ line: line + 1,
153
+ column: character + 1,
154
+ dynamic: literal === null,
155
+ pattern: calleeName
156
+ });
157
+ }
158
+ }
159
+ }
160
+ import_typescript.default.forEachChild(node, visit);
161
+ };
162
+ visit(sourceFile);
163
+ return usages.filter((usage) => usage.key || usage.dynamic);
164
+ }
165
+ function extractLiteralUsages(filePath, content, prefix) {
166
+ if (!prefix) {
167
+ return [];
168
+ }
169
+ const escapedPrefix = prefix.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
170
+ const expression = new RegExp(`(["'\`])(${escapedPrefix}[A-Za-z0-9_.-]+)\\1`, "g");
171
+ const usages = [];
172
+ let match;
173
+ while ((match = expression.exec(content)) !== null) {
174
+ const fullKey = match[2];
175
+ const before = content.slice(0, match.index);
176
+ const lines = before.split("\n");
177
+ const line = lines.length;
178
+ const column = lines[lines.length - 1].length + 1;
179
+ usages.push({
180
+ key: fullKey,
181
+ file: filePath,
182
+ line,
183
+ column,
184
+ dynamic: false,
185
+ pattern: "literal"
186
+ });
187
+ }
188
+ return usages;
189
+ }
190
+ function getCalleeName(expression) {
191
+ if (import_typescript.default.isIdentifier(expression)) {
192
+ return expression.text;
193
+ }
194
+ if (import_typescript.default.isPropertyAccessExpression(expression)) {
195
+ const left = getCalleeName(expression.expression);
196
+ if (!left) {
197
+ return expression.name.text;
198
+ }
199
+ return `${left}.${expression.name.text}`;
200
+ }
201
+ return null;
202
+ }
203
+ function getLiteralText(expression) {
204
+ if (import_typescript.default.isStringLiteral(expression)) {
205
+ return expression.text;
206
+ }
207
+ if (import_typescript.default.isNoSubstitutionTemplateLiteral(expression)) {
208
+ return expression.text;
209
+ }
210
+ return null;
211
+ }
212
+
213
+ // src/core/analyze.ts
214
+ async function analyzeProject(params) {
215
+ const config = applyCliOverrides(params.config, params.cliOptions);
216
+ const translationFiles = discoverLocales(config);
217
+ const sourceFiles = await discoverSourceFiles(config);
218
+ const codeUsages = collectCodeUsages(sourceFiles, config);
219
+ const issues = collectIssues(translationFiles, codeUsages, config, params.cliOptions);
220
+ const namespaces = buildNamespaces(translationFiles);
221
+ const summary = buildSummary(translationFiles, codeUsages, issues);
222
+ return {
223
+ config,
224
+ translationFiles,
225
+ codeUsages,
226
+ issues,
227
+ summary,
228
+ namespaces
229
+ };
230
+ }
231
+ function applyCliOverrides(config, cli) {
232
+ if (!cli) {
233
+ return config;
234
+ }
235
+ return {
236
+ ...config,
237
+ source: cli.source && cli.source.length ? cli.source : config.source,
238
+ locales: {
239
+ ...config.locales,
240
+ dir: cli.localesDir ?? config.locales.dir,
241
+ sourceLang: cli.sourceLang ?? config.locales.sourceLang
242
+ },
243
+ ignore: cli.ignore && cli.ignore.length ? [...config.ignore, ...cli.ignore] : config.ignore
244
+ };
245
+ }
246
+ function collectCodeUsages(files, config) {
247
+ const usages = [];
248
+ for (const file of files) {
249
+ const content = import_fs2.default.readFileSync(file, "utf8");
250
+ const fileUsages = extractCodeUsages(file, content, config);
251
+ const literalUsages = extractLiteralUsages(file, content, config.patterns.prefix);
252
+ usages.push(...fileUsages, ...literalUsages);
253
+ }
254
+ const normalized = usages.map((usage) => {
255
+ const prefix = config.patterns.prefix;
256
+ if (prefix && usage.key.startsWith(prefix)) {
257
+ return { ...usage, key: usage.key.slice(prefix.length) };
258
+ }
259
+ return usage;
260
+ });
261
+ return dedupeUsages(normalized);
262
+ }
263
+ function dedupeUsages(usages) {
264
+ const seen = /* @__PURE__ */ new Set();
265
+ return usages.filter((usage) => {
266
+ const token = `${usage.key}:${usage.file}:${usage.line}:${usage.column}:${usage.dynamic}`;
267
+ if (seen.has(token)) {
268
+ return false;
269
+ }
270
+ seen.add(token);
271
+ return true;
272
+ });
273
+ }
274
+ function collectIssues(translations, codeUsages, config, cli) {
275
+ const issues = [];
276
+ const locales = translations.map((file) => file.locale);
277
+ const reference = translations.find((file) => file.locale === config.locales.sourceLang);
278
+ const referenceKeys = reference ? new Set(reference.flat.keys()) : /* @__PURE__ */ new Set();
279
+ const allKeys = /* @__PURE__ */ new Set();
280
+ for (const file of translations) {
281
+ for (const key of file.flat.keys()) {
282
+ allKeys.add(key);
283
+ }
284
+ }
285
+ for (const locale of locales) {
286
+ const file = translations.find((entry) => entry.locale === locale);
287
+ if (!file) {
288
+ continue;
289
+ }
290
+ const keysToCheck = reference ? referenceKeys : allKeys;
291
+ for (const key of keysToCheck) {
292
+ if (!file.flat.has(key)) {
293
+ issues.push({
294
+ type: "missing",
295
+ severity: "high",
296
+ key,
297
+ locale,
298
+ message: `Missing key "${key}" in ${locale}`
299
+ });
300
+ }
301
+ }
302
+ if (reference) {
303
+ for (const key of file.flat.keys()) {
304
+ if (!referenceKeys.has(key)) {
305
+ issues.push({
306
+ type: "extra",
307
+ severity: "medium",
308
+ key,
309
+ locale: file.locale,
310
+ message: `Extra key "${key}" in ${file.locale}`
311
+ });
312
+ }
313
+ }
314
+ }
315
+ }
316
+ if (config.rules.detectUnused) {
317
+ const usedKeys = new Set(codeUsages.filter((usage) => !usage.dynamic).map((u) => u.key));
318
+ const referenceKeys2 = reference ? Array.from(reference.flat.keys()) : Array.from(allKeys);
319
+ for (const key of referenceKeys2) {
320
+ if (!usedKeys.has(key)) {
321
+ issues.push({
322
+ type: "unused",
323
+ severity: "low",
324
+ key,
325
+ message: `Unused key "${key}"`
326
+ });
327
+ }
328
+ }
329
+ }
330
+ if (config.rules.detectEmpty || config.rules.detectNull || config.rules.detectSameAsKey) {
331
+ for (const file of translations) {
332
+ for (const [key, value] of file.flat.entries()) {
333
+ if (config.rules.detectNull && value === null) {
334
+ issues.push({
335
+ type: "null",
336
+ severity: "medium",
337
+ key,
338
+ locale: file.locale,
339
+ message: `Null value for "${key}" in ${file.locale}`
340
+ });
341
+ }
342
+ if (config.rules.detectEmpty && value === "") {
343
+ issues.push({
344
+ type: "empty",
345
+ severity: "medium",
346
+ key,
347
+ locale: file.locale,
348
+ message: `Empty value for "${key}" in ${file.locale}`
349
+ });
350
+ }
351
+ if (config.rules.detectSameAsKey && typeof value === "string" && value.trim() === key) {
352
+ issues.push({
353
+ type: "same-as-key",
354
+ severity: "low",
355
+ key,
356
+ locale: file.locale,
357
+ message: `Value equals key for "${key}" in ${file.locale}`
358
+ });
359
+ }
360
+ }
361
+ }
362
+ }
363
+ if (config.rules.detectDuplicateTranslations) {
364
+ for (const key of allKeys) {
365
+ const values = translations.map((file) => file.flat.get(key)).filter((value) => typeof value === "string");
366
+ if (values.length === locales.length && new Set(values).size === 1) {
367
+ issues.push({
368
+ type: "duplicate-value",
369
+ severity: "low",
370
+ key,
371
+ message: `Same translation for all locales: "${key}"`
372
+ });
373
+ }
374
+ }
375
+ }
376
+ if (config.rules.detectPlaceholderMismatch) {
377
+ for (const key of allKeys) {
378
+ const placeholderMap = /* @__PURE__ */ new Map();
379
+ for (const file of translations) {
380
+ const value = file.flat.get(key);
381
+ if (typeof value === "string") {
382
+ placeholderMap.set(file.locale, extractPlaceholders(value));
383
+ }
384
+ }
385
+ const reference2 = placeholderMap.get(config.locales.sourceLang) ?? [];
386
+ for (const [locale, placeholders] of placeholderMap.entries()) {
387
+ if (!samePlaceholders(reference2, placeholders)) {
388
+ issues.push({
389
+ type: "placeholder",
390
+ severity: "medium",
391
+ key,
392
+ locale,
393
+ message: `Placeholder mismatch for "${key}" in ${locale}`
394
+ });
395
+ }
396
+ }
397
+ }
398
+ }
399
+ if (config.rules.detectStructureMismatch) {
400
+ const structure = /* @__PURE__ */ new Map();
401
+ for (const file of translations) {
402
+ structure.set(file.locale, new Set(file.flat.keys()));
403
+ }
404
+ for (const key of allKeys) {
405
+ for (const locale of locales) {
406
+ const localeKeys = structure.get(locale);
407
+ if (!localeKeys) continue;
408
+ if (!localeKeys.has(key) && localeKeysHasPrefix(localeKeys, key)) {
409
+ issues.push({
410
+ type: "structure",
411
+ severity: "medium",
412
+ key,
413
+ locale,
414
+ message: `Structure mismatch around "${key}" in ${locale}`
415
+ });
416
+ }
417
+ }
418
+ }
419
+ }
420
+ if (cli?.includeDynamicWarnings) {
421
+ for (const usage of codeUsages.filter((usage2) => usage2.dynamic)) {
422
+ issues.push({
423
+ type: "dynamic",
424
+ severity: "low",
425
+ key: usage.key,
426
+ file: usage.file,
427
+ message: `Dynamic key usage in ${import_path2.default.basename(usage.file)}`
428
+ });
429
+ }
430
+ }
431
+ return issues;
432
+ }
433
+ function buildSummary(translations, codeUsages, issues) {
434
+ const summary = {
435
+ locales: translations.map((file) => file.locale),
436
+ totalKeys: new Set(translations.flatMap((file) => Array.from(file.flat.keys()))).size,
437
+ totalUsages: codeUsages.length,
438
+ totalIssues: issues.length,
439
+ missingKeys: 0,
440
+ extraKeys: 0,
441
+ unusedKeys: 0,
442
+ emptyValues: 0,
443
+ nullValues: 0,
444
+ sameAsKey: 0,
445
+ placeholderIssues: 0,
446
+ duplicateValueIssues: 0,
447
+ structureIssues: 0,
448
+ dynamicWarnings: 0
449
+ };
450
+ for (const issue of issues) {
451
+ switch (issue.type) {
452
+ case "missing":
453
+ summary.missingKeys += 1;
454
+ break;
455
+ case "extra":
456
+ summary.extraKeys += 1;
457
+ break;
458
+ case "unused":
459
+ summary.unusedKeys += 1;
460
+ break;
461
+ case "empty":
462
+ summary.emptyValues += 1;
463
+ break;
464
+ case "null":
465
+ summary.nullValues += 1;
466
+ break;
467
+ case "same-as-key":
468
+ summary.sameAsKey += 1;
469
+ break;
470
+ case "placeholder":
471
+ summary.placeholderIssues += 1;
472
+ break;
473
+ case "duplicate-value":
474
+ summary.duplicateValueIssues += 1;
475
+ break;
476
+ case "structure":
477
+ summary.structureIssues += 1;
478
+ break;
479
+ case "dynamic":
480
+ summary.dynamicWarnings += 1;
481
+ break;
482
+ default:
483
+ break;
484
+ }
485
+ }
486
+ return summary;
487
+ }
488
+ function buildNamespaces(translations) {
489
+ const namespaces = {};
490
+ for (const file of translations) {
491
+ for (const key of file.flat.keys()) {
492
+ const prefix = key.includes(".") ? key.split(".")[0] : "root";
493
+ namespaces[prefix] = (namespaces[prefix] ?? 0) + 1;
494
+ }
495
+ }
496
+ return namespaces;
497
+ }
498
+ function extractPlaceholders(value) {
499
+ const placeholders = /* @__PURE__ */ new Set();
500
+ const mustache = value.match(/\{\{(\w+)\}\}/g) ?? [];
501
+ const brace = value.match(/\{(\w+)\}/g) ?? [];
502
+ const percent = value.match(/%[sdif]/g) ?? [];
503
+ for (const token of mustache) placeholders.add(token);
504
+ for (const token of brace) placeholders.add(token);
505
+ for (const token of percent) placeholders.add(token);
506
+ return Array.from(placeholders).sort();
507
+ }
508
+ function samePlaceholders(left, right) {
509
+ if (left.length !== right.length) {
510
+ return false;
511
+ }
512
+ for (let index = 0; index < left.length; index += 1) {
513
+ if (left[index] !== right[index]) {
514
+ return false;
515
+ }
516
+ }
517
+ return true;
518
+ }
519
+ function localeKeysHasPrefix(keys, key) {
520
+ const prefix = `${key}.`;
521
+ for (const candidate of keys) {
522
+ if (candidate.startsWith(prefix)) {
523
+ return true;
524
+ }
525
+ }
526
+ return false;
527
+ }
528
+
529
+ // src/config/loadConfig.ts
530
+ var import_fs3 = __toESM(require("fs"));
531
+ var import_path3 = __toESM(require("path"));
532
+
533
+ // src/config/defaults.ts
534
+ var DEFAULT_CONFIG = {
535
+ projectRoot: process.cwd(),
536
+ source: ["src"],
537
+ locales: {
538
+ dir: "src/i18n",
539
+ extension: ".json",
540
+ fileName: "trad",
541
+ sourceLang: "en"
542
+ },
543
+ patterns: {
544
+ prefix: "trad.",
545
+ functions: ["t", "i18n.t", "translate.instant", "this.translate.instant"]
546
+ },
547
+ ignore: ["**/node_modules/**", "**/dist/**", "**/.git/**", "**/coverage/**"],
548
+ rules: {
549
+ detectEmpty: true,
550
+ detectNull: true,
551
+ detectSameAsKey: true,
552
+ detectUnused: true,
553
+ detectDuplicateTranslations: true,
554
+ detectPlaceholderMismatch: true,
555
+ detectStructureMismatch: true
556
+ },
557
+ report: {
558
+ title: "Translint Report",
559
+ darkMode: true
560
+ },
561
+ fix: {
562
+ strategy: "todo",
563
+ placeholder: "TODO_TRANSLATE",
564
+ sort: true
565
+ }
566
+ };
567
+
568
+ // src/utils/merge.ts
569
+ function mergeDeep(base, override) {
570
+ if (override === void 0 || override === null) {
571
+ return base;
572
+ }
573
+ if (Array.isArray(base) && Array.isArray(override)) {
574
+ return override;
575
+ }
576
+ if (isObject(base) && isObject(override)) {
577
+ const result = { ...base };
578
+ for (const [key, value] of Object.entries(override)) {
579
+ const current = base[key];
580
+ result[key] = mergeDeep(current, value);
581
+ }
582
+ return result;
583
+ }
584
+ return override;
585
+ }
586
+ function isObject(value) {
587
+ return typeof value === "object" && value !== null && !Array.isArray(value);
588
+ }
589
+
590
+ // src/config/loadConfig.ts
591
+ var CONFIG_FILES = [
592
+ "i18n-checker.config.js",
593
+ "i18n-checker.config.cjs",
594
+ "i18n-checker.config.mjs",
595
+ "i18n-checker.config.json",
596
+ "i18n-checker.config.ts"
597
+ ];
598
+ async function loadConfig(configPath) {
599
+ if (configPath) {
600
+ return mergeDeep(DEFAULT_CONFIG, await loadConfigFile(configPath));
601
+ }
602
+ for (const fileName of CONFIG_FILES) {
603
+ const candidate = import_path3.default.resolve(process.cwd(), fileName);
604
+ if (import_fs3.default.existsSync(candidate)) {
605
+ return mergeDeep(DEFAULT_CONFIG, await loadConfigFile(candidate));
606
+ }
607
+ }
608
+ const packageJsonPath = import_path3.default.resolve(process.cwd(), "package.json");
609
+ if (import_fs3.default.existsSync(packageJsonPath)) {
610
+ const pkg = JSON.parse(import_fs3.default.readFileSync(packageJsonPath, "utf8"));
611
+ if (pkg.i18nChecker) {
612
+ return mergeDeep(DEFAULT_CONFIG, pkg.i18nChecker);
613
+ }
614
+ }
615
+ return DEFAULT_CONFIG;
616
+ }
617
+ async function loadConfigFile(filePath) {
618
+ const ext = import_path3.default.extname(filePath);
619
+ if (ext === ".json") {
620
+ return JSON.parse(import_fs3.default.readFileSync(filePath, "utf8"));
621
+ }
622
+ if (ext === ".ts") {
623
+ try {
624
+ await import("ts-node/register/transpile-only");
625
+ } catch (error) {
626
+ throw new Error(
627
+ "Config file is TypeScript but ts-node is not installed. Install ts-node or use .js/.json."
628
+ );
629
+ }
630
+ }
631
+ const resolved = import_path3.default.isAbsolute(filePath) ? filePath : import_path3.default.resolve(process.cwd(), filePath);
632
+ const imported = require(resolved);
633
+ return imported.default ?? imported;
634
+ }
635
+
636
+ // src/scoring/score.ts
637
+ function computeScore(analysis) {
638
+ const { summary } = analysis;
639
+ const completeness = scorePart(summary.missingKeys + summary.extraKeys, summary.totalKeys);
640
+ const usage = scorePart(summary.unusedKeys, summary.totalKeys);
641
+ const consistency = scorePart(summary.placeholderIssues + summary.structureIssues, summary.totalKeys);
642
+ const quality = scorePart(
643
+ summary.emptyValues + summary.nullValues + summary.sameAsKey + summary.duplicateValueIssues,
644
+ summary.totalKeys
645
+ );
646
+ const total = Math.round(
647
+ completeness * 0.35 + consistency * 0.25 + usage * 0.2 + quality * 0.2
648
+ );
649
+ const level = total >= 90 ? "excellent" : total >= 75 ? "good" : total >= 60 ? "average" : "critical";
650
+ const notes = [];
651
+ if (summary.missingKeys > 0) notes.push("Fix missing keys across locales.");
652
+ if (summary.unusedKeys > 0) notes.push("Clean unused translation keys.");
653
+ if (summary.placeholderIssues > 0) notes.push("Align placeholders between languages.");
654
+ if (summary.emptyValues > 0 || summary.nullValues > 0)
655
+ notes.push("Fill empty or null translations.");
656
+ return {
657
+ total,
658
+ level,
659
+ breakdown: {
660
+ completeness,
661
+ consistency,
662
+ usage,
663
+ quality
664
+ },
665
+ notes
666
+ };
667
+ }
668
+ function scorePart(issues, total) {
669
+ if (total === 0) return 100;
670
+ const ratio = Math.max(0, Math.min(1, 1 - issues / total));
671
+ return Math.round(ratio * 100);
672
+ }
673
+ // Annotate the CommonJS export names for ESM import in node:
674
+ 0 && (module.exports = {
675
+ analyzeProject,
676
+ computeScore,
677
+ loadConfig
678
+ });