langcleaner 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/README.md +153 -0
- package/bin/index.js +142 -0
- package/package.json +41 -0
- package/src/commands/applyCodeUpdates.js +60 -0
- package/src/commands/dedupe.js +85 -0
- package/src/commands/findMissingTranslations.js +115 -0
- package/src/commands/findUnusedKeys.js +114 -0
- package/src/commands/syncAll.js +48 -0
- package/src/commands/syncLang.js +74 -0
- package/src/commands/syncLangCore.js +64 -0
- package/src/utils/dedupeUtils.js +103 -0
- package/src/utils/fileUtils.js +59 -0
- package/src/utils/sortUtils.js +28 -0
package/README.md
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
# 🌍 LangCleaner
|
|
2
|
+
|
|
3
|
+
**The intelligent i18n maintenance toolkit for nested or flat JSON.**
|
|
4
|
+
|
|
5
|
+
LangCleaner is a CLI tool designed to keep your internationalization files clean, synchronized, and perfectly matched to your source code. It doesn't just find problems—it fixes them.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 🚀 The Maintenance Pipeline
|
|
10
|
+
|
|
11
|
+
The most powerful way to use this tool is the **maintenance command**. It runs a 5-step "Self-Healing" cycle where each step builds upon the last:
|
|
12
|
+
|
|
13
|
+
1. **Dedupe**
|
|
14
|
+
Finds multiple keys with the same translation (e.g., `Save` and `Apply` both being `"Submit"`) and merges them.
|
|
15
|
+
|
|
16
|
+
2. **Apply Code Updates**
|
|
17
|
+
Automatically renames those keys in your React/Vue/JS source code to match the new merged keys.
|
|
18
|
+
|
|
19
|
+
3. **Find Unused Keys**
|
|
20
|
+
Scans your code, finds and deletes (if you add --fix) any keys from your JSON that are no longer used.
|
|
21
|
+
|
|
22
|
+
4. **Find Missing Translations**
|
|
23
|
+
Finds keys you wrote in your code (like `t("nav.home")`) that don't exist in your JSON yet and adds (if you add --fix) them as empty stubs.
|
|
24
|
+
|
|
25
|
+
5. **Sync Lang / Sync All**
|
|
26
|
+
Takes your now-perfect English (or other master) file and forces all other languages (`tr.json`, `de.json`, etc.) to match its structure perfectly.
|
|
27
|
+
|
|
28
|
+
### Maintenance Pipeline Diagram
|
|
29
|
+
|
|
30
|
+
```plaintext
|
|
31
|
+
+---------+
|
|
32
|
+
| Start |
|
|
33
|
+
+----+----+
|
|
34
|
+
|
|
|
35
|
+
v
|
|
36
|
+
+-----+-----+
|
|
37
|
+
| Dedupe |
|
|
38
|
+
+-----+-----+
|
|
39
|
+
|
|
|
40
|
+
v
|
|
41
|
+
+----------+-----------+
|
|
42
|
+
| Apply Updates |
|
|
43
|
+
+----------+-----------+
|
|
44
|
+
|
|
|
45
|
+
v
|
|
46
|
+
+-------+-------+
|
|
47
|
+
| Clean Unused |
|
|
48
|
+
+-------+-------+
|
|
49
|
+
|
|
|
50
|
+
v
|
|
51
|
+
+-------+-------+
|
|
52
|
+
| Add Missing |
|
|
53
|
+
+-------+-------+
|
|
54
|
+
|
|
|
55
|
+
v
|
|
56
|
+
+-----+-----+
|
|
57
|
+
| Sync All |
|
|
58
|
+
+-----+-----+
|
|
59
|
+
|
|
|
60
|
+
v
|
|
61
|
+
+----+----+
|
|
62
|
+
| Finish |
|
|
63
|
+
+---------+
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## 📦 Installation
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
npm install -g langcleaner
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## 🛠 Commands & Usage
|
|
77
|
+
|
|
78
|
+
### ⚡ Full Maintenance (Recommended)
|
|
79
|
+
|
|
80
|
+
Run the entire pipeline in one go:
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
langcleaner maintenance ./locales/en.json ./src --fix
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
**Options:**
|
|
87
|
+
|
|
88
|
+
* `--fix`: Required to actually update your files. Without it, the tool only generates reports.
|
|
89
|
+
|
|
90
|
+
### 🔍 Individual Commands
|
|
91
|
+
|
|
92
|
+
| Command | Purpose | Sample Usage |
|
|
93
|
+
| --------------------------- | ------------------------------------ | ------------------------------------------------------------- |
|
|
94
|
+
| `dedupe` | Merges duplicate values in JSON | `langcleaner dedupe ./locales/en.json --fix` |
|
|
95
|
+
| `apply-code-updates` | Updates code based on dedupe mapping | `langcleaner apply-code-updates ./output/update-mapping.json ./src` |
|
|
96
|
+
| `find-unused-keys` | Removes dead keys from JSON | `langcleaner find-unused-keys ./locales/en.json ./src --fix` |
|
|
97
|
+
| `find-missing-translations` | Stubs out missing keys in JSON | `langcleaner find-missing-translations ./locales/en.json ./src --fix` |
|
|
98
|
+
| `sync-lang` | Syncs a language to match master | `langcleaner sync-lang ./locales/en.json ./locales/de.json` |
|
|
99
|
+
| `sync-all` | Syncs all languages to match master | `langcleaner sync-all ./locales/en.json ./locales` |
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
## 📂 Understanding the `output/` folder
|
|
104
|
+
|
|
105
|
+
LangCleaner follows a **Safety-First** approach. It never overwrites your primary master file. Instead, it creates an `output/` directory:
|
|
106
|
+
|
|
107
|
+
```plaintext
|
|
108
|
+
output/
|
|
109
|
+
└── en/
|
|
110
|
+
├── cleaned-en.json <-- Your "Gold Standard" file
|
|
111
|
+
├── update-mapping.json <-- Logic used to update code
|
|
112
|
+
├── unused-keys-report.json <-- Audit of what was deleted
|
|
113
|
+
└── missing-keys-report.json <-- Audit of what needs translation
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### Example JSON Snippets
|
|
117
|
+
|
|
118
|
+
**Before `dedupe`:**
|
|
119
|
+
|
|
120
|
+
```json
|
|
121
|
+
{
|
|
122
|
+
"save": "Submit",
|
|
123
|
+
"apply": "Submit",
|
|
124
|
+
"cancel": "Cancel"
|
|
125
|
+
}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
**After `dedupe`:**
|
|
129
|
+
|
|
130
|
+
```json
|
|
131
|
+
{
|
|
132
|
+
"save": "Submit",
|
|
133
|
+
"cancel": "Cancel"
|
|
134
|
+
}
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### Professional Audit Reports
|
|
138
|
+
|
|
139
|
+
Every report is standardized for your team's review:
|
|
140
|
+
|
|
141
|
+
* **Config:** Shows absolute paths and files scanned.
|
|
142
|
+
* **Summary:** High-level stats (e.g., "37% of keys were unused").
|
|
143
|
+
* **Details:** Alphabetical lists of exactly which keys were changed.
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
147
|
+
## 💡 Pro-Tips
|
|
148
|
+
|
|
149
|
+
* **Always use a Master:** Choose one language (usually English) as your master path.
|
|
150
|
+
* **Dry Run:** Run commands without `--fix` first to see the JSON reports and verify changes.
|
|
151
|
+
* **Commit First:** Always commit your code to Git before running with `--fix` so you can easily revert if needed.
|
|
152
|
+
|
|
153
|
+
---
|
package/bin/index.js
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { program } from "commander";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import fs from "fs";
|
|
5
|
+
|
|
6
|
+
// Command Imports
|
|
7
|
+
import dedupe from "../src/commands/dedupe.js";
|
|
8
|
+
import applyCodeUpdates from "../src/commands/applyCodeUpdates.js";
|
|
9
|
+
import findUnusedKeys from "../src/commands/findUnusedKeys.js";
|
|
10
|
+
import findMissingTranslations from "../src/commands/findMissingTranslations.js";
|
|
11
|
+
import syncLang from "../src/commands/syncLang.js";
|
|
12
|
+
import syncAll from "../src/commands/syncAll.js";
|
|
13
|
+
|
|
14
|
+
program
|
|
15
|
+
.name("langcleaner")
|
|
16
|
+
.description("I18n nested JSON cleaner & synchronization CLI tool")
|
|
17
|
+
.version("1.0.0");
|
|
18
|
+
|
|
19
|
+
/* --- THE SNOWBALL EFFECT ---
|
|
20
|
+
Each command follows a prioritized path logic:
|
|
21
|
+
1. Check for a "previously cleaned" version in ./output/{lang}/cleaned-{file}.json.
|
|
22
|
+
2. If not found, fallback to the original source path.
|
|
23
|
+
This allows the maintenance pipeline to build upon the results of previous steps.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* 1. DEDUPE
|
|
28
|
+
* Merges keys sharing identical values.
|
|
29
|
+
* Sample: langcleaner dedupe ./locales/en.json --fix --sort
|
|
30
|
+
*/
|
|
31
|
+
program
|
|
32
|
+
.command("dedupe <masterPath>")
|
|
33
|
+
.description("Find repetitive values and generate an update-mapping.json")
|
|
34
|
+
.option("--fix", "Generate a cleaned JSON file in the output folder")
|
|
35
|
+
.option("--sort", "Sort keys alphabetically in the output file")
|
|
36
|
+
.action(dedupe);
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* 2. APPLY CODE UPDATES
|
|
40
|
+
* Renames keys in source files based on dedupe mapping.
|
|
41
|
+
* Sample: langcleaner apply-code-updates ./output/en/update-mapping.json ./src
|
|
42
|
+
*/
|
|
43
|
+
program
|
|
44
|
+
.command("apply-code-updates <mappingPath> <projectDir>")
|
|
45
|
+
.description("Update source code references based on the dedupe mapping file")
|
|
46
|
+
.action(applyCodeUpdates);
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* 3. FIND UNUSED KEYS
|
|
50
|
+
* Removes keys from JSON that aren't found in the code.
|
|
51
|
+
* Sample: langcleaner find-unused-keys ./locales/en.json ./src --fix
|
|
52
|
+
*/
|
|
53
|
+
program
|
|
54
|
+
.command("find-unused-keys <masterPath> <projectDir>")
|
|
55
|
+
.description("Find and optionally remove i18n keys not referenced in code")
|
|
56
|
+
.option("--fix", "Generate a cleaned version (removes unused keys)")
|
|
57
|
+
.action(async (masterPath, projectDir, options) => {
|
|
58
|
+
await findUnusedKeys(masterPath, projectDir, options.fix);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* 4. FIND MISSING TRANSLATIONS
|
|
63
|
+
* Detects keys in code missing from JSON.
|
|
64
|
+
* Sample: langcleaner find-missing-translations ./locales/en.json ./src --fix
|
|
65
|
+
*/
|
|
66
|
+
program
|
|
67
|
+
.command("find-missing-translations <masterPath> <projectDir>")
|
|
68
|
+
.description("Find keys present in code but missing from the master JSON file")
|
|
69
|
+
.option("--fix", "Automatically add missing keys to the cleaned file as stubs")
|
|
70
|
+
.action(async (masterPath, projectDir, options) => {
|
|
71
|
+
await findMissingTranslations(masterPath, projectDir, options.fix);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* 5. SYNC SINGLE LANGUAGE
|
|
76
|
+
* Syncs one specific target to match the master structure.
|
|
77
|
+
* Sample: langcleaner sync-lang ./locales/en.json ./locales/tr.json
|
|
78
|
+
*/
|
|
79
|
+
program
|
|
80
|
+
.command("sync-lang <masterPath> <targetPath>")
|
|
81
|
+
.description("Sync a single target file structure to match the master file")
|
|
82
|
+
.action(syncLang);
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* 6. SYNC ALL LANGUAGES
|
|
86
|
+
* Bulk syncs all files in a folder against the master.
|
|
87
|
+
* Sample: langcleaner sync-all ./locales/en.json ./locales
|
|
88
|
+
*/
|
|
89
|
+
program
|
|
90
|
+
.command("sync-all <masterPath> <localesDir>")
|
|
91
|
+
.description("Sync all JSON files in a directory to match the master structure")
|
|
92
|
+
.action(syncAll);
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* 7. FULL MAINTENANCE (THE AUTOMATED PIPELINE)
|
|
96
|
+
* Runs the full sequence: Dedupe -> Apply -> Unused -> Missing -> Sync.
|
|
97
|
+
* Sample: langcleaner maintenance ./locales/en.json ./src --fix
|
|
98
|
+
*/
|
|
99
|
+
program
|
|
100
|
+
.command("maintenance <masterPath> <projectDir>")
|
|
101
|
+
.description("Run full cycle: Dedupe -> Update Code -> Clean Unused -> Find Missing -> Sync All")
|
|
102
|
+
.option("--fix", "Automatically apply changes to JSON and source code")
|
|
103
|
+
.action(async (masterPath, projectDir, options) => {
|
|
104
|
+
const langName = path.basename(masterPath, ".json");
|
|
105
|
+
const langFileName = path.basename(masterPath);
|
|
106
|
+
const outDir = path.join(process.cwd(), "output", langName);
|
|
107
|
+
const mappingFile = path.join(outDir, "update-mapping.json");
|
|
108
|
+
const cleanedFile = path.join(outDir, `cleaned-${langFileName}`);
|
|
109
|
+
|
|
110
|
+
console.log("\n🚀 Starting Full Project Maintenance...");
|
|
111
|
+
console.log("──────────────────────────────────────────────────");
|
|
112
|
+
|
|
113
|
+
// Step 1: Dedupe (Initial Clean)
|
|
114
|
+
console.log("\n[1/5] Deduping values...");
|
|
115
|
+
await dedupe(masterPath, { fix: options.fix, sort: true });
|
|
116
|
+
|
|
117
|
+
// Step 2: Update Code (Requires mapping from Step 1)
|
|
118
|
+
if (options.fix) {
|
|
119
|
+
console.log("\n[2/5] Updating source code references...");
|
|
120
|
+
await applyCodeUpdates(mappingFile, projectDir);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Step 3: Remove Unused (Refines Clean)
|
|
124
|
+
console.log("\n[3/5] Cleaning unused keys...");
|
|
125
|
+
await findUnusedKeys(masterPath, projectDir, options.fix);
|
|
126
|
+
|
|
127
|
+
// Step 4: Audit/Add Missing (Finalizes Clean)
|
|
128
|
+
console.log("\n[4/5] Checking and adding missing translations...");
|
|
129
|
+
await findMissingTranslations(masterPath, projectDir, options.fix);
|
|
130
|
+
|
|
131
|
+
// Step 5: Global Synchronization
|
|
132
|
+
// Priority: Use the fully processed 'cleaned' file if it exists, otherwise use original.
|
|
133
|
+
const syncSource = (options.fix && fs.existsSync(cleanedFile)) ? cleanedFile : masterPath;
|
|
134
|
+
|
|
135
|
+
console.log(`\n[5/5] Syncing all languages using: ${path.basename(syncSource)}...`);
|
|
136
|
+
await syncAll(syncSource, path.dirname(masterPath));
|
|
137
|
+
|
|
138
|
+
console.log("\n──────────────────────────────────────────────────");
|
|
139
|
+
console.log("✅ Maintenance completed! Review results in the 'output' folder.");
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
program.parse();
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "langcleaner",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Smart i18n maintenance tool: dedupe keys, prune unused translations, and sync nested JSON structures.",
|
|
5
|
+
"main": "bin/index.js",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"langcleaner": "./bin/index.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"start": "node bin/index.js",
|
|
12
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
13
|
+
},
|
|
14
|
+
"keywords": [
|
|
15
|
+
"i18n",
|
|
16
|
+
"internationalization",
|
|
17
|
+
"json-cleaner",
|
|
18
|
+
"dedupe",
|
|
19
|
+
"unused-keys",
|
|
20
|
+
"localization",
|
|
21
|
+
"translate",
|
|
22
|
+
"refactor",
|
|
23
|
+
"cli"
|
|
24
|
+
],
|
|
25
|
+
"author": "Recep Yalcin",
|
|
26
|
+
"license": "MIT",
|
|
27
|
+
"repository": {
|
|
28
|
+
"type": "git",
|
|
29
|
+
"url": "git+https://github.com/recyalcin/langcleaner.git"
|
|
30
|
+
},
|
|
31
|
+
"bugs": {
|
|
32
|
+
"url": "https://github.com/recyalcin/langcleaner/issues"
|
|
33
|
+
},
|
|
34
|
+
"homepage": "https://github.com/recyalcin/langcleaner#readme",
|
|
35
|
+
"dependencies": {
|
|
36
|
+
"commander": "^11.1.0"
|
|
37
|
+
},
|
|
38
|
+
"engines": {
|
|
39
|
+
"node": ">=16.0.0"
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { getAllFiles, loadJson } from "../utils/fileUtils.js";
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
export default async function applyCodeUpdates(mappingFile, projectPath) {
|
|
7
|
+
if (!fs.existsSync(mappingFile)) {
|
|
8
|
+
console.error(`❌ Error: Mapping file not found at ${mappingFile}`);
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// updateMapping: { "auth.login.old": "auth.login.new" }
|
|
13
|
+
const { updateMapping } = loadJson(mappingFile);
|
|
14
|
+
const files = getAllFiles(projectPath, [".js", ".jsx", ".ts", ".tsx", ".vue", ".html", ".svelte"]);
|
|
15
|
+
|
|
16
|
+
console.log(`\n🛠 Updating code references...`);
|
|
17
|
+
console.log(`──────────────────────────────────────────────────`);
|
|
18
|
+
|
|
19
|
+
let fileCount = 0;
|
|
20
|
+
let replacementCount = 0;
|
|
21
|
+
|
|
22
|
+
files.forEach(file => {
|
|
23
|
+
let content = fs.readFileSync(file, "utf8");
|
|
24
|
+
let isModified = false;
|
|
25
|
+
|
|
26
|
+
for (const [oldKey, newKey] of Object.entries(updateMapping)) {
|
|
27
|
+
/**
|
|
28
|
+
* REGEX STRATEGY:
|
|
29
|
+
* 1. (['"`]) : Opening quote (single, double, or backtick)
|
|
30
|
+
* 2. ${escapedOldKey} : The exact i18n path to be replaced
|
|
31
|
+
* 3. (['"`]) : Closing quote
|
|
32
|
+
* * This structure catches:
|
|
33
|
+
* - t("auth.button")
|
|
34
|
+
* - translation['auth.button']
|
|
35
|
+
* - [ "auth.button" ]
|
|
36
|
+
* - i18nKey="auth.button"
|
|
37
|
+
*/
|
|
38
|
+
const escapedOldKey = oldKey.replace(/\./g, "\\.");
|
|
39
|
+
const regex = new RegExp(`(['"\`])${escapedOldKey}(['"\`])`, "g");
|
|
40
|
+
|
|
41
|
+
if (regex.test(content)) {
|
|
42
|
+
// $1 and $2 put the captured quotes back, replacing only the key inside
|
|
43
|
+
content = content.replace(regex, `$1${newKey}$2`);
|
|
44
|
+
isModified = true;
|
|
45
|
+
replacementCount++;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (isModified) {
|
|
50
|
+
fs.writeFileSync(file, content, "utf8");
|
|
51
|
+
console.log(` ✔ Updated: ${path.relative(projectPath, file)}`);
|
|
52
|
+
fileCount++;
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
console.log(`──────────────────────────────────────────────────`);
|
|
57
|
+
console.log(`✅ Update Complete!`);
|
|
58
|
+
console.log(` • ${replacementCount} key references replaced.`);
|
|
59
|
+
console.log(` • ${fileCount} files updated.`);
|
|
60
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import { loadJson, writeJson, ensureDir } from "../utils/fileUtils.js";
|
|
3
|
+
import { dedupeValues } from "../utils/dedupeUtils.js";
|
|
4
|
+
import { sortJsonObject } from "../utils/sortUtils.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Finds repetitive values in the language file, reports them, and optionally cleans them.
|
|
8
|
+
* @param {string} file - Path to the language file (e.g., ./locales/en.json)
|
|
9
|
+
* @param {object} options - CLI options (--fix, --sort)
|
|
10
|
+
*/
|
|
11
|
+
export default async function dedupe(file, options) {
|
|
12
|
+
if (!file) {
|
|
13
|
+
console.error("❌ Error: Please provide a valid language file path.");
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// 1. Prepare File and Language Info
|
|
18
|
+
const json = loadJson(file);
|
|
19
|
+
const fileName = path.basename(file);
|
|
20
|
+
const langName = path.basename(file, ".json");
|
|
21
|
+
const absoluteSourcePath = path.resolve(file); // Get full path for the report
|
|
22
|
+
|
|
23
|
+
// 2. Set Output Directory
|
|
24
|
+
const outDir = path.join(process.cwd(), "output", langName);
|
|
25
|
+
ensureDir(outDir);
|
|
26
|
+
|
|
27
|
+
console.log(`\n🔍 Analyzing [${fileName}]...`);
|
|
28
|
+
|
|
29
|
+
// 3. Deduplication Logic
|
|
30
|
+
const { cleaned, removedValues, updateMapping } = dedupeValues(json);
|
|
31
|
+
const duplicateCount = Object.keys(removedValues).length;
|
|
32
|
+
|
|
33
|
+
// 4. Enhanced Report Data (Matching the format of other commands)
|
|
34
|
+
const reportPath = path.join(outDir, `repetitive-keys-report.json`);
|
|
35
|
+
const mappingPath = path.join(outDir, `update-mapping.json`);
|
|
36
|
+
|
|
37
|
+
const reportData = {
|
|
38
|
+
timestamp: new Date().toISOString(),
|
|
39
|
+
config: {
|
|
40
|
+
sourceFile: fileName,
|
|
41
|
+
absoluteSourcePath: absoluteSourcePath,
|
|
42
|
+
isCleaned: !!options.fix,
|
|
43
|
+
isSorted: !!options.sort
|
|
44
|
+
},
|
|
45
|
+
summary: {
|
|
46
|
+
totalDuplicatesFound: duplicateCount
|
|
47
|
+
},
|
|
48
|
+
details: {
|
|
49
|
+
duplicates: removedValues,
|
|
50
|
+
mapping: updateMapping
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
// Write reports
|
|
55
|
+
writeJson(reportPath, reportData);
|
|
56
|
+
|
|
57
|
+
if (duplicateCount > 0) {
|
|
58
|
+
// We save a clean version of the mapping file for the code-updater to read easily
|
|
59
|
+
writeJson(mappingPath, { updateMapping });
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// 5. FIX & SORT Operations
|
|
63
|
+
if (options.fix && duplicateCount > 0) {
|
|
64
|
+
let finalJson = cleaned;
|
|
65
|
+
|
|
66
|
+
if (options.sort) {
|
|
67
|
+
console.log(" Sorting keys alphabetically (recursive)...");
|
|
68
|
+
finalJson = sortJsonObject(cleaned);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const cleanedFile = path.join(outDir, `cleaned-${fileName}`);
|
|
72
|
+
writeJson(cleanedFile, finalJson);
|
|
73
|
+
|
|
74
|
+
console.log(` ✅ Success: Cleaned file created at ${cleanedFile}`);
|
|
75
|
+
} else if (duplicateCount > 0) {
|
|
76
|
+
console.log(` ℹ️ ${duplicateCount} duplicates found. Use --fix to clean them.`);
|
|
77
|
+
} else {
|
|
78
|
+
console.log(` ✅ Great! No repetitive values found.`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
console.log(` 📄 Report: ${reportPath}`);
|
|
82
|
+
if (duplicateCount > 0) {
|
|
83
|
+
console.log(` 🔗 Mapping: ${mappingPath} (Ready for apply-code-updates)`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { getAllFiles, loadJson, writeJson, ensureDir } from "../utils/fileUtils.js";
|
|
4
|
+
|
|
5
|
+
/* ------------------ HELPERS ------------------ */
|
|
6
|
+
|
|
7
|
+
function extractKeysFromContent(content) {
|
|
8
|
+
const keys = new Set();
|
|
9
|
+
const patterns = [
|
|
10
|
+
/\b\$?t\s*\(\s*["'`]([^"'`$]+?)["'`]/g,
|
|
11
|
+
/\b(?:i18n|i18next)\.t\s*\(\s*["'`]([^"'`$]+?)["'`]/g,
|
|
12
|
+
/(?:id|i18nKey)=["'`]([^"'`]+?)["'`]/g,
|
|
13
|
+
/\[\s*["'`]([^"'`$]+?)["'`]\s*\]/g
|
|
14
|
+
];
|
|
15
|
+
for (const pattern of patterns) {
|
|
16
|
+
let match;
|
|
17
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
18
|
+
if (match[1] && match[1].trim().length > 0) keys.add(match[1]);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return keys;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function flattenKeys(obj, prefix = "", res = {}) {
|
|
25
|
+
for (const key in obj) {
|
|
26
|
+
const value = obj[key];
|
|
27
|
+
const newKey = prefix ? `${prefix}.${key}` : key;
|
|
28
|
+
if (typeof value === "object" && value !== null && !Array.isArray(value)) {
|
|
29
|
+
flattenKeys(value, newKey, res);
|
|
30
|
+
} else {
|
|
31
|
+
res[newKey] = value;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return res;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function setDeep(obj, path, value) {
|
|
38
|
+
const parts = path.split(".");
|
|
39
|
+
let current = obj;
|
|
40
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
41
|
+
const part = parts[i];
|
|
42
|
+
if (!current[part] || typeof current[part] !== "object") {
|
|
43
|
+
current[part] = {};
|
|
44
|
+
}
|
|
45
|
+
current = current[part];
|
|
46
|
+
}
|
|
47
|
+
const lastPart = parts[parts.length - 1];
|
|
48
|
+
if (!(lastPart in current)) {
|
|
49
|
+
current[lastPart] = value;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/* ------------------ MAIN COMMAND ------------------ */
|
|
54
|
+
|
|
55
|
+
export default async function findMissingTranslations(masterFile, projectDir, fix = false) {
|
|
56
|
+
if (!fs.existsSync(masterFile)) {
|
|
57
|
+
console.error(`❌ Error: Master language file not found at ${masterFile}`);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const langFileName = path.basename(masterFile);
|
|
62
|
+
const langName = path.basename(masterFile, ".json");
|
|
63
|
+
const outDir = path.join(process.cwd(), "output", langName);
|
|
64
|
+
|
|
65
|
+
const previouslyCleaned = path.join(outDir, `cleaned-${langFileName}`);
|
|
66
|
+
const sourcePath = fs.existsSync(previouslyCleaned) ? previouslyCleaned : masterFile;
|
|
67
|
+
|
|
68
|
+
ensureDir(outDir);
|
|
69
|
+
|
|
70
|
+
const masterData = loadJson(sourcePath);
|
|
71
|
+
const masterKeys = new Set(Object.keys(flattenKeys(masterData)));
|
|
72
|
+
const files = getAllFiles(projectDir, [".js", ".jsx", ".ts", ".tsx", ".vue", ".html", ".svelte"]);
|
|
73
|
+
const usedInCode = new Set();
|
|
74
|
+
|
|
75
|
+
for (const file of files) {
|
|
76
|
+
const content = fs.readFileSync(file, "utf-8");
|
|
77
|
+
extractKeysFromContent(content).forEach(k => usedInCode.add(k));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const missing = Array.from(usedInCode).filter(key => !masterKeys.has(key));
|
|
81
|
+
|
|
82
|
+
if (fix && missing.length > 0) {
|
|
83
|
+
missing.forEach(key => setDeep(masterData, key, ""));
|
|
84
|
+
writeJson(path.join(outDir, `cleaned-${langFileName}`), masterData);
|
|
85
|
+
console.log(`✨ Success: Added ${missing.length} stub keys to cleaned file.`);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// --- UPDATED REPORT STRUCTURE ---
|
|
89
|
+
const reportPath = path.join(outDir, "missing-keys-report.json");
|
|
90
|
+
const reportData = {
|
|
91
|
+
timestamp: new Date().toISOString(),
|
|
92
|
+
config: {
|
|
93
|
+
comparedAgainst: sourcePath,
|
|
94
|
+
projectDirectory: path.resolve(projectDir), // Shows the full absolute path of the scanned code
|
|
95
|
+
totalFilesScanned: files.length
|
|
96
|
+
},
|
|
97
|
+
summary: {
|
|
98
|
+
uniqueKeysInCode: usedInCode.size,
|
|
99
|
+
missingKeysCount: missing.length
|
|
100
|
+
},
|
|
101
|
+
missingKeys: missing.sort()
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
writeJson(reportPath, reportData);
|
|
105
|
+
|
|
106
|
+
console.log(`──────────────────────────────────────────`);
|
|
107
|
+
if (missing.length === 0) {
|
|
108
|
+
console.log(`✅ Success: All keys found in code are defined.`);
|
|
109
|
+
} else {
|
|
110
|
+
console.log(`❌ Found ${missing.length} keys missing from ${path.basename(sourcePath)}:`);
|
|
111
|
+
missing.slice(0, 10).forEach(k => console.log(` • ${k}`));
|
|
112
|
+
if (missing.length > 10) console.log(` • ...and ${missing.length - 10} more.`);
|
|
113
|
+
console.log(`\n📂 Report saved to: ${reportPath}`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { getAllFiles, ensureDir, writeJson, loadJson } from "../utils/fileUtils.js";
|
|
4
|
+
|
|
5
|
+
/* ------------------ HELPERS ------------------ */
|
|
6
|
+
|
|
7
|
+
function flattenKeys(obj, prefix = "", res = {}) {
|
|
8
|
+
for (const key in obj) {
|
|
9
|
+
const value = obj[key];
|
|
10
|
+
const newKey = prefix ? `${prefix}.${key}` : key;
|
|
11
|
+
if (typeof value === "object" && value !== null && !Array.isArray(value)) {
|
|
12
|
+
flattenKeys(value, newKey, res);
|
|
13
|
+
} else {
|
|
14
|
+
res[newKey] = value;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return res;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function removeEmptyObjects(obj) {
|
|
21
|
+
for (const key in obj) {
|
|
22
|
+
if (typeof obj[key] === 'object' && obj[key] !== null && !Array.isArray(obj[key])) {
|
|
23
|
+
removeEmptyObjects(obj[key]);
|
|
24
|
+
if (Object.keys(obj[key]).length === 0) {
|
|
25
|
+
delete obj[key];
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function deepDelete(obj, keyPath) {
|
|
32
|
+
const parts = keyPath.split('.');
|
|
33
|
+
const last = parts.pop();
|
|
34
|
+
const target = parts.reduce((acc, part) => acc && acc[part], obj);
|
|
35
|
+
if (target && typeof target === 'object' && last in target) {
|
|
36
|
+
delete target[last];
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/* ------------------ MAIN COMMAND ------------------ */
|
|
41
|
+
|
|
42
|
+
export default async function findUnusedKeys(langFilePath, projectPath, fix = false) {
|
|
43
|
+
if (!fs.existsSync(langFilePath)) {
|
|
44
|
+
console.error(`❌ Error: Language file not found at ${langFilePath}`);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const langFileName = path.basename(langFilePath);
|
|
49
|
+
const langName = path.basename(langFilePath, ".json");
|
|
50
|
+
const outDir = path.join(process.cwd(), "output", langName);
|
|
51
|
+
|
|
52
|
+
const previouslyCleaned = path.join(outDir, `cleaned-${langFileName}`);
|
|
53
|
+
const sourcePath = fs.existsSync(previouslyCleaned) ? previouslyCleaned : langFilePath;
|
|
54
|
+
|
|
55
|
+
if (sourcePath === previouslyCleaned) {
|
|
56
|
+
console.log(`💡 Note: Using previously cleaned file as source: ${previouslyCleaned}`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
ensureDir(outDir);
|
|
60
|
+
console.log(`\n🔍 Scanning project for unused keys in [${langName}]...`);
|
|
61
|
+
|
|
62
|
+
const langData = loadJson(sourcePath);
|
|
63
|
+
const flatLangKeys = Object.keys(flattenKeys(langData));
|
|
64
|
+
const projectFiles = getAllFiles(projectPath, [".js", ".jsx", ".ts", ".tsx", ".vue", ".html", ".svelte"]);
|
|
65
|
+
|
|
66
|
+
let allProjectContent = "";
|
|
67
|
+
for (const file of projectFiles) {
|
|
68
|
+
allProjectContent += fs.readFileSync(file, "utf-8") + "\n";
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const unused = flatLangKeys.filter(key => {
|
|
72
|
+
const escapedKey = key.replace(/\./g, "\\.");
|
|
73
|
+
const regex = new RegExp(`(['"\`])${escapedKey}(['"\`])`, "g");
|
|
74
|
+
return !regex.test(allProjectContent);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
const totalKeys = flatLangKeys.length;
|
|
78
|
+
const unusedCount = unused.length;
|
|
79
|
+
|
|
80
|
+
if (fix && unusedCount > 0) {
|
|
81
|
+
unused.forEach(key => deepDelete(langData, key));
|
|
82
|
+
removeEmptyObjects(langData);
|
|
83
|
+
const cleanFilePath = path.join(outDir, `cleaned-${langFileName}`);
|
|
84
|
+
writeJson(cleanFilePath, langData);
|
|
85
|
+
console.log(`✨ Success: Updated cleaned file created at ${cleanFilePath}`);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// --- STANDARDIZED REPORT STRUCTURE ---
|
|
89
|
+
const reportPath = path.join(outDir, "unused-keys-report.json");
|
|
90
|
+
const reportData = {
|
|
91
|
+
timestamp: new Date().toISOString(),
|
|
92
|
+
config: {
|
|
93
|
+
sourceUsed: path.resolve(sourcePath),
|
|
94
|
+
projectDirectory: path.resolve(projectPath),
|
|
95
|
+
totalFilesScanned: projectFiles.length,
|
|
96
|
+
isCleaned: !!fix
|
|
97
|
+
},
|
|
98
|
+
summary: {
|
|
99
|
+
totalKeysInJSON: totalKeys,
|
|
100
|
+
unusedCount: unusedCount,
|
|
101
|
+
usedCount: totalKeys - unusedCount,
|
|
102
|
+
percentageUnused: totalKeys > 0 ? ((unusedCount / totalKeys) * 100).toFixed(2) + "%" : "0%"
|
|
103
|
+
},
|
|
104
|
+
details: {
|
|
105
|
+
unusedKeys: unused.sort()
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
writeJson(reportPath, reportData);
|
|
110
|
+
|
|
111
|
+
console.log(`──────────────────────────────────────────`);
|
|
112
|
+
console.log(`✅ Used: ${totalKeys - unusedCount} | ❌ Unused: ${unusedCount}`);
|
|
113
|
+
console.log(`📂 Report saved to: ${reportPath}`);
|
|
114
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import syncLang from "./syncLang.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Syncs all JSON files in a directory with the master language file.
|
|
7
|
+
* Automatically excludes the master file from being treated as a target.
|
|
8
|
+
*/
|
|
9
|
+
export default async function syncAll(masterFile, dirPath) {
|
|
10
|
+
if (!fs.existsSync(dirPath)) {
|
|
11
|
+
console.error(`❌ Error: Locales directory not found at ${dirPath}`);
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const masterAbsPath = path.resolve(masterFile);
|
|
16
|
+
const masterName = path.basename(masterFile);
|
|
17
|
+
|
|
18
|
+
// Find other .json files in the folder (excluding the master file)
|
|
19
|
+
const files = fs.readdirSync(dirPath).filter(f => {
|
|
20
|
+
const fullPath = path.resolve(path.join(dirPath, f));
|
|
21
|
+
return f.endsWith(".json") && fullPath !== masterAbsPath;
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
console.log(`\n🌍 Bulk Sync Started`);
|
|
25
|
+
console.log(`──────────────────────────────────`);
|
|
26
|
+
console.log(`Master Source: ${masterName}`);
|
|
27
|
+
console.log(`Target Count: ${files.length} files\n`);
|
|
28
|
+
|
|
29
|
+
const results = [];
|
|
30
|
+
|
|
31
|
+
for (const file of files) {
|
|
32
|
+
const fullPath = path.join(dirPath, file);
|
|
33
|
+
try {
|
|
34
|
+
await syncLang(masterFile, fullPath);
|
|
35
|
+
results.push({ lang: file, status: "✔ Success" });
|
|
36
|
+
} catch (error) {
|
|
37
|
+
results.push({ lang: file, status: "✖ Failed", error: error.message });
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
console.log(`──────────────────────────────────`);
|
|
42
|
+
console.log(`🏁 Sync All Completed!`);
|
|
43
|
+
results.forEach(res => {
|
|
44
|
+
const statusMsg = res.status.padEnd(12);
|
|
45
|
+
const errorDetail = res.error ? `(Error: ${res.error})` : "";
|
|
46
|
+
console.log(`${statusMsg} ${res.lang} ${errorDetail}`);
|
|
47
|
+
});
|
|
48
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import { ensureDir, writeJson } from "../utils/fileUtils.js";
|
|
4
|
+
import { syncSingleLang } from "./syncLangCore.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Syncs a specific target translation file with the master file.
|
|
8
|
+
*/
|
|
9
|
+
export default async function syncLang(masterFile, targetFile) {
|
|
10
|
+
if (!fs.existsSync(masterFile) || !fs.existsSync(targetFile)) {
|
|
11
|
+
console.error(`❌ Error: One or both translation files are missing.`);
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const masterName = path.basename(masterFile);
|
|
16
|
+
const targetName = path.basename(targetFile);
|
|
17
|
+
const langCode = path.basename(targetFile, '.json');
|
|
18
|
+
|
|
19
|
+
console.log(`🔄 Syncing: ${targetName} ← ${masterName}`);
|
|
20
|
+
|
|
21
|
+
// 1. Perform recursive synchronization
|
|
22
|
+
const { result, missing, extra } = syncSingleLang(masterFile, targetFile);
|
|
23
|
+
|
|
24
|
+
// 2. Set output directory (e.g., output/tr for tr.json)
|
|
25
|
+
const outDir = path.join(process.cwd(), "output", langCode);
|
|
26
|
+
ensureDir(outDir);
|
|
27
|
+
|
|
28
|
+
// 3. Write updated translation file
|
|
29
|
+
const outFile = path.join(outDir, targetName);
|
|
30
|
+
writeJson(outFile, result);
|
|
31
|
+
|
|
32
|
+
// Helper to count total leaf nodes (actual translation strings)
|
|
33
|
+
function countKeys(obj) {
|
|
34
|
+
let count = 0;
|
|
35
|
+
for (const key in obj) {
|
|
36
|
+
if (typeof obj[key] === 'object' && obj[key] !== null && !Array.isArray(obj[key])) {
|
|
37
|
+
count += countKeys(obj[key]);
|
|
38
|
+
} else {
|
|
39
|
+
count++;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return count;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// 4. STANDARDIZED REPORT GENERATION
|
|
46
|
+
const reportData = {
|
|
47
|
+
timestamp: new Date().toISOString(),
|
|
48
|
+
config: {
|
|
49
|
+
masterSource: path.resolve(masterFile),
|
|
50
|
+
targetSource: path.resolve(targetFile),
|
|
51
|
+
masterFileName: masterName,
|
|
52
|
+
targetFileName: targetName
|
|
53
|
+
},
|
|
54
|
+
summary: {
|
|
55
|
+
missingKeysAdded: missing.length,
|
|
56
|
+
extraKeysRemoved: extra.length,
|
|
57
|
+
totalKeysInTarget: countKeys(result)
|
|
58
|
+
},
|
|
59
|
+
details: {
|
|
60
|
+
addedKeys: missing.sort(), // Alphabetical sort for readability
|
|
61
|
+
removedKeys: extra.sort() // Alphabetical sort for readability
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const reportFile = path.join(outDir, `sync-report.json`);
|
|
66
|
+
writeJson(reportFile, reportData);
|
|
67
|
+
|
|
68
|
+
console.log(` ✔ File saved: ${outFile}`);
|
|
69
|
+
console.log(` ✔ Report: ${reportFile}`);
|
|
70
|
+
console.log(` • Added: ${missing.length} missing keys | Removed: ${extra.length} obsolete keys\n`);
|
|
71
|
+
|
|
72
|
+
// Return data for syncAll if needed
|
|
73
|
+
return { success: true, target: targetName, report: reportData };
|
|
74
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { loadJson } from "../utils/fileUtils.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Recursive function to synchronize nested objects.
|
|
5
|
+
* Deeply identifies missing keys and extra keys based on a master object.
|
|
6
|
+
*/
|
|
7
|
+
function recursiveSync(master, target = {}, currentPath = "") {
|
|
8
|
+
const result = {};
|
|
9
|
+
const missing = [];
|
|
10
|
+
const extra = [];
|
|
11
|
+
|
|
12
|
+
// 1. Iterate through Master (Build structure and find missing keys)
|
|
13
|
+
for (const key in master) {
|
|
14
|
+
const fullPath = currentPath ? `${currentPath}.${key}` : key;
|
|
15
|
+
const masterValue = master[key];
|
|
16
|
+
const targetValue = target && typeof target === 'object' ? target[key] : undefined;
|
|
17
|
+
|
|
18
|
+
if (typeof masterValue === 'object' && masterValue !== null && !Array.isArray(masterValue)) {
|
|
19
|
+
// If it's an object, dive deeper
|
|
20
|
+
const sub = recursiveSync(masterValue, targetValue || {}, fullPath);
|
|
21
|
+
result[key] = sub.result;
|
|
22
|
+
missing.push(...sub.missing);
|
|
23
|
+
extra.push(...sub.extra);
|
|
24
|
+
} else {
|
|
25
|
+
// If it's a value: keep target value if it exists, otherwise add empty string
|
|
26
|
+
if (target && key in target) {
|
|
27
|
+
result[key] = target[key];
|
|
28
|
+
} else {
|
|
29
|
+
result[key] = ""; // New key added as empty
|
|
30
|
+
missing.push(fullPath);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// 2. Identify keys in Target that do not exist in Master (Extra/Obsolete keys)
|
|
36
|
+
if (target && typeof target === 'object') {
|
|
37
|
+
for (const key in target) {
|
|
38
|
+
const fullPath = currentPath ? `${currentPath}.${key}` : key;
|
|
39
|
+
if (!(key in master)) {
|
|
40
|
+
extra.push(fullPath);
|
|
41
|
+
// We don't add these to 'result', so they are effectively removed (Cleanup)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return { result, missing, extra };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Synchronizes a single target language file with the master file.
|
|
51
|
+
*/
|
|
52
|
+
export function syncSingleLang(masterFile, targetFile) {
|
|
53
|
+
const master = loadJson(masterFile);
|
|
54
|
+
const target = loadJson(targetFile);
|
|
55
|
+
|
|
56
|
+
// Start the recursive synchronization process
|
|
57
|
+
const { result, missing, extra } = recursiveSync(master, target);
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
result,
|
|
61
|
+
missing,
|
|
62
|
+
extra
|
|
63
|
+
};
|
|
64
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 🔵 1. Duplicate KEY temizleme (JSON.parse sonrası)
|
|
3
|
+
* Not: Standart JSON.parse zaten son anahtarı alır.
|
|
4
|
+
* Bu fonksiyon manuel inşa edilen objeler için recursive çalışır.
|
|
5
|
+
*/
|
|
6
|
+
export function dedupeKeys(obj, strategy = "first") {
|
|
7
|
+
const cleaned = {};
|
|
8
|
+
const removedKeys = {};
|
|
9
|
+
|
|
10
|
+
for (const key in obj) {
|
|
11
|
+
const value = obj[key];
|
|
12
|
+
|
|
13
|
+
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
|
14
|
+
const sub = dedupeKeys(value, strategy);
|
|
15
|
+
cleaned[key] = sub.cleaned;
|
|
16
|
+
if (Object.keys(sub.removedKeys).length > 0) {
|
|
17
|
+
removedKeys[key] = sub.removedKeys;
|
|
18
|
+
}
|
|
19
|
+
} else {
|
|
20
|
+
cleaned[key] = value;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return { cleaned, removedKeys };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* 🔵 2. Duplicate VALUE temizleme (JSON Object Mode - Recursive)
|
|
29
|
+
* Tüm hiyerarşiyi tarar ve aynı değere sahip anahtarları bulur.
|
|
30
|
+
*/
|
|
31
|
+
export function dedupeValues(obj) {
|
|
32
|
+
const removedValues = {};
|
|
33
|
+
const valueMap = {}; // value -> firstKeyPath
|
|
34
|
+
const updateMapping = {}; // duplicateKeyPath -> firstKeyPath
|
|
35
|
+
|
|
36
|
+
function walk(currentObj, path = "") {
|
|
37
|
+
const result = {};
|
|
38
|
+
|
|
39
|
+
for (const key in currentObj) {
|
|
40
|
+
const fullPath = path ? `${path}.${key}` : key;
|
|
41
|
+
const value = currentObj[key];
|
|
42
|
+
|
|
43
|
+
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
|
44
|
+
result[key] = walk(value, fullPath);
|
|
45
|
+
// Eğer iç obje boş kaldıysa temizle (isteğe bağlı)
|
|
46
|
+
if (Object.keys(result[key]).length === 0) delete result[key];
|
|
47
|
+
} else {
|
|
48
|
+
// Değer kontrolü (Normalizasyon: Boşlukları temizle)
|
|
49
|
+
const normalized = String(value).trim();
|
|
50
|
+
|
|
51
|
+
if (!Object.prototype.hasOwnProperty.call(valueMap, normalized)) {
|
|
52
|
+
valueMap[normalized] = fullPath;
|
|
53
|
+
result[key] = value;
|
|
54
|
+
} else {
|
|
55
|
+
removedValues[fullPath] = value;
|
|
56
|
+
updateMapping[fullPath] = valueMap[normalized];
|
|
57
|
+
// Duplicate olduğu için result'a eklemiyoruz
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return result;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const cleaned = walk(obj);
|
|
65
|
+
return { cleaned, removedValues, updateMapping };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* 🔵 3. Duplicate VALUE temizleme (RAW MODE)
|
|
70
|
+
* i18n dosyalarında yorumları korumak kritikse kullanılır.
|
|
71
|
+
* (Genelde düz yapılar için daha sağlıklıdır)
|
|
72
|
+
*/
|
|
73
|
+
export function dedupeValuesRaw(rawText) {
|
|
74
|
+
const lines = rawText.split("\n");
|
|
75
|
+
const valueMap = {};
|
|
76
|
+
const removedValues = {};
|
|
77
|
+
const updateMapping = {};
|
|
78
|
+
const outputLines = [];
|
|
79
|
+
|
|
80
|
+
const keyValueRegex = /^\s*"([^"]+)"\s*:\s*"([\s\S]*?)"\s*(,)?\s*$/;
|
|
81
|
+
|
|
82
|
+
for (const line of lines) {
|
|
83
|
+
const match = line.match(keyValueRegex);
|
|
84
|
+
if (!match) {
|
|
85
|
+
outputLines.push(line);
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const key = match[1];
|
|
90
|
+
const value = match[2];
|
|
91
|
+
const normalized = value.replace(/\s+/g, " ").trim();
|
|
92
|
+
|
|
93
|
+
if (!Object.prototype.hasOwnProperty.call(valueMap, normalized)) {
|
|
94
|
+
valueMap[normalized] = key;
|
|
95
|
+
outputLines.push(line);
|
|
96
|
+
} else {
|
|
97
|
+
removedValues[key] = value;
|
|
98
|
+
updateMapping[key] = valueMap[normalized];
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return { cleanedText: outputLines.join("\n"), removedValues, updateMapping };
|
|
103
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Loads and parses a JSON file.
|
|
6
|
+
*/
|
|
7
|
+
export function loadJson(file) {
|
|
8
|
+
try {
|
|
9
|
+
return JSON.parse(fs.readFileSync(file, "utf-8"));
|
|
10
|
+
} catch (error) {
|
|
11
|
+
console.error(`❌ Error parsing JSON at ${file}:`, error.message);
|
|
12
|
+
return {};
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Writes an object to a JSON file with 2-space indentation.
|
|
18
|
+
*/
|
|
19
|
+
export function writeJson(file, data) {
|
|
20
|
+
fs.writeFileSync(file, JSON.stringify(data, null, 2), "utf-8");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Ensures a directory exists, creating it recursively if needed.
|
|
25
|
+
*/
|
|
26
|
+
export function ensureDir(dir) {
|
|
27
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Generates a standardized output path: output/[langName]
|
|
32
|
+
*/
|
|
33
|
+
export function getOutputDir(file) {
|
|
34
|
+
const fileName = path.basename(file, path.extname(file));
|
|
35
|
+
const outputDir = path.join("output", fileName);
|
|
36
|
+
ensureDir(outputDir);
|
|
37
|
+
return outputDir;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Recursively scans a directory for files with specific extensions.
|
|
42
|
+
* Centralized here to keep commands dry (Don't Repeat Yourself).
|
|
43
|
+
*/
|
|
44
|
+
export function getAllFiles(dir, exts, files = []) {
|
|
45
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
46
|
+
for (const entry of entries) {
|
|
47
|
+
const fullPath = path.join(dir, entry.name);
|
|
48
|
+
|
|
49
|
+
if (entry.isDirectory()) {
|
|
50
|
+
// Ignore unnecessary directories to speed up scanning
|
|
51
|
+
if (!["node_modules", ".git", "dist", "build", ".next", "out"].includes(entry.name)) {
|
|
52
|
+
getAllFiles(fullPath, exts, files);
|
|
53
|
+
}
|
|
54
|
+
} else if (exts.includes(path.extname(entry.name))) {
|
|
55
|
+
files.push(fullPath);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return files;
|
|
59
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JSON objesini derinlemesine (recursive) alfabetik olarak sıralar.
|
|
3
|
+
*/
|
|
4
|
+
export function sortJsonObject(obj) {
|
|
5
|
+
// Eğer gelen veri bir obje değilse veya null ise direkt döndür (recursion sonu)
|
|
6
|
+
if (typeof obj !== "object" || obj === null || Array.isArray(obj)) {
|
|
7
|
+
return obj;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// 1. Mevcut objenin anahtarlarını al ve sırala
|
|
11
|
+
const sortedKeys = Object.keys(obj).sort((a, b) => a.localeCompare(b));
|
|
12
|
+
|
|
13
|
+
const result = {};
|
|
14
|
+
|
|
15
|
+
// 2. Her bir anahtar için derinleş
|
|
16
|
+
for (const key of sortedKeys) {
|
|
17
|
+
const value = obj[key];
|
|
18
|
+
|
|
19
|
+
// Eğer değer bir objeyse, onun içine de girip sırala
|
|
20
|
+
if (typeof value === "object" && value !== null && !Array.isArray(value)) {
|
|
21
|
+
result[key] = sortJsonObject(value);
|
|
22
|
+
} else {
|
|
23
|
+
result[key] = value;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return result;
|
|
28
|
+
}
|