@tuhama/translation-manager 0.2.0 โ 0.4.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/LICENSE +21 -21
- package/README.md +71 -59
- package/bin/index.js +45 -45
- package/package.json +66 -66
- package/src/core/Scanner.js +128 -128
- package/src/core/Storage.js +82 -82
- package/src/core/TranslatorManager.js +229 -204
- package/src/core/Utilities.js +55 -55
- package/src/core/services/GoogleTranslator.js +69 -51
- package/web/dist/assets/{index-CvijWYi5.js โ index-ByUmql2E.js} +1 -1
- package/web/dist/index.html +18 -18
- package/web/package.json +22 -22
package/LICENSE
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2026 Tuhama
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Tuhama
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,59 +1,71 @@
|
|
|
1
|
-
# Translation Manager UI ๐
|
|
2
|
-
|
|
3
|
-
A modern, web-based interface for managing i18n translation files in React and other JavaScript projects.
|
|
4
|
-
|
|
5
|
-
## Features
|
|
6
|
-
- **Modern UI**: Dark mode, glassmorphism, and smooth animations.
|
|
7
|
-
- **Auto-Translation**: Integrated Google Translate support for single-key and bulk translations.
|
|
8
|
-
- **Missing Keys Detection**: Identifies translation keys used in source code but missing from files.
|
|
9
|
-
- **Cleanup Tool**: Detects and batch-removes unused translation keys.
|
|
10
|
-
- **Normalization**: Synchronizes keys across all languages and sorts them alphabetically with one click.
|
|
11
|
-
- **Nested Keys**: Supports dot-notation for nested JSON structures.
|
|
12
|
-
- **Tree View**: Easy navigation and management of translation keys.
|
|
13
|
-
- **Zero Config**: Auto-detects common locales folders.
|
|
14
|
-
|
|
15
|
-
## Installation
|
|
16
|
-
Add it as a devDependency to your project:
|
|
17
|
-
```bash
|
|
18
|
-
npm install -g @tuhama/translation-manager
|
|
19
|
-
```
|
|
20
|
-
Or run directly with npx:
|
|
21
|
-
```bash
|
|
22
|
-
npx @tuhama/translation-manager
|
|
23
|
-
```
|
|
24
|
-
|
|
25
|
-
## Usage
|
|
26
|
-
|
|
27
|
-
### โ ๏ธ Missing Keys Detection
|
|
28
|
-
The application automatically scans your source code for translation keys used (e.g., `t('key.name')`) but missing from your translation files. Click the "**Missing**" button in the sidebar to review and create them instantly.
|
|
29
|
-
|
|
30
|
-
### ๐งน Cleaning Unused Keys
|
|
31
|
-
Over time, some translation keys might become obsolete. Use the "**Clean**" button to identify and batch-delete keys that are no longer referenced in your source code.
|
|
32
|
-
|
|
33
|
-
### ๐ช Auto-Translation
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
```
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
- **
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
1
|
+
# Translation Manager UI ๐
|
|
2
|
+
|
|
3
|
+
A modern, web-based interface for managing i18n translation files in React and other JavaScript projects.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
- **Modern UI**: Dark mode, glassmorphism, and smooth animations.
|
|
7
|
+
- **Auto-Translation**: Integrated Google Translate support for single-key and bulk translations.
|
|
8
|
+
- **Missing Keys Detection**: Identifies translation keys used in source code but missing from files.
|
|
9
|
+
- **Cleanup Tool**: Detects and batch-removes unused translation keys.
|
|
10
|
+
- **Normalization**: Synchronizes keys across all languages and sorts them alphabetically with one click.
|
|
11
|
+
- **Nested Keys**: Supports dot-notation for nested JSON structures.
|
|
12
|
+
- **Tree View**: Easy navigation and management of translation keys.
|
|
13
|
+
- **Zero Config**: Auto-detects common locales folders.
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
Add it as a devDependency to your project:
|
|
17
|
+
```bash
|
|
18
|
+
npm install -g @tuhama/translation-manager
|
|
19
|
+
```
|
|
20
|
+
Or run directly with npx:
|
|
21
|
+
```bash
|
|
22
|
+
npx @tuhama/translation-manager
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Usage
|
|
26
|
+
|
|
27
|
+
### โ ๏ธ Missing Keys Detection
|
|
28
|
+
The application automatically scans your source code for translation keys used (e.g., `t('key.name')`) but missing from your translation files. Click the "**Missing**" button in the sidebar to review and create them instantly.
|
|
29
|
+
|
|
30
|
+
### ๐งน Cleaning Unused Keys
|
|
31
|
+
Over time, some translation keys might become obsolete. Use the "**Clean**" button to identify and batch-delete keys that are no longer referenced in your source code.
|
|
32
|
+
|
|
33
|
+
### ๐ช Auto-Translation
|
|
34
|
+
Configure Google Cloud Translation API in the settings to enable auto-translation. Use the "**Source-to-All**" button in the editor to quickly populate all languages from a single source translation.
|
|
35
|
+
|
|
36
|
+
**Recommended Setup:**
|
|
37
|
+
1. Install Google Cloud CLI: `gcloud auth application-default login`
|
|
38
|
+
2. Add your Google Cloud Project ID in Settings
|
|
39
|
+
3. Start translating!
|
|
40
|
+
|
|
41
|
+
### ๐ช Normalization
|
|
42
|
+
To keep your translation files organized, use the "**Normalize**" button to synchronize keys across all files and sort them alphabetically.
|
|
43
|
+
|
|
44
|
+
## Configuration
|
|
45
|
+
You can optionally create a `translation.config.json` in your project root:
|
|
46
|
+
```json
|
|
47
|
+
{
|
|
48
|
+
"path": "src/locales",
|
|
49
|
+
"googleTranslate": {
|
|
50
|
+
"projectId": "your-google-cloud-project-id"
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
**Authentication Options:**
|
|
56
|
+
- **Recommended**: Use Google Cloud CLI (`gcloud auth application-default login`)
|
|
57
|
+
- **Alternative**: Specify `keyFilename` path to service account JSON file
|
|
58
|
+
- **Environment**: Set `GOOGLE_APPLICATION_CREDENTIALS` environment variable
|
|
59
|
+
|
|
60
|
+
## Development
|
|
61
|
+
To work on this repo:
|
|
62
|
+
1. `npm install`
|
|
63
|
+
2. `cd web && npm install`
|
|
64
|
+
3. `npm run dev` (starts both the API and the Vite UI)
|
|
65
|
+
|
|
66
|
+
## Limitations
|
|
67
|
+
- **Dynamic Keys**: The scanner uses regex to find translation keys. Highly dynamic keys (e.g. `t(someVar + '.key')` or `t(dynamicValue)`) may not be detected by the "Missing Keys" or "Unused Keys" tools.
|
|
68
|
+
- **Namespaces**: Currently optimized for single-namespace or default-namespace projects.
|
|
69
|
+
|
|
70
|
+
## License
|
|
71
|
+
MIT ยฉ [Tuhama](mailto:tuhama.gh.qlyshi@gmail.com)
|
package/bin/index.js
CHANGED
|
@@ -1,45 +1,45 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
const { program } = require('commander');
|
|
4
|
-
const path = require('path');
|
|
5
|
-
const fs = require('fs-extra');
|
|
6
|
-
const { startServer } = require('../src/server');
|
|
7
|
-
const open = (...args) => import('open').then(m => m.default(...args));
|
|
8
|
-
|
|
9
|
-
program
|
|
10
|
-
.version('0.1.0')
|
|
11
|
-
.description('Translation Manager CLI - Manage your React translations with a modern UI')
|
|
12
|
-
.option('-p, --port <number>', 'Port to run the UI on', 3000)
|
|
13
|
-
.option('-c, --config <path>', 'Path to config file')
|
|
14
|
-
.action(async (options) => {
|
|
15
|
-
const targetDir = process.cwd();
|
|
16
|
-
let config = {};
|
|
17
|
-
|
|
18
|
-
// Load config from file
|
|
19
|
-
const configPath = options.config || 'translation.config.json';
|
|
20
|
-
const absoluteConfigPath = path.resolve(targetDir, configPath);
|
|
21
|
-
|
|
22
|
-
if (await fs.pathExists(absoluteConfigPath)) {
|
|
23
|
-
config = await fs.readJson(absoluteConfigPath);
|
|
24
|
-
} else {
|
|
25
|
-
// Check for JS config
|
|
26
|
-
const jsConfigPath = path.resolve(targetDir, 'translation.config.js');
|
|
27
|
-
if (await fs.pathExists(jsConfigPath)) {
|
|
28
|
-
config = require(jsConfigPath);
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
console.log('\x1b[36mโน\x1b[0m Starting Translation Manager...');
|
|
33
|
-
|
|
34
|
-
try {
|
|
35
|
-
startServer(targetDir, options.port, config);
|
|
36
|
-
|
|
37
|
-
// Open the browser
|
|
38
|
-
await open(`http://localhost:${options.port}`);
|
|
39
|
-
} catch (err) {
|
|
40
|
-
console.error('\x1b[31mโ\x1b[0m Error:', err.message);
|
|
41
|
-
process.exit(1);
|
|
42
|
-
}
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
program.parse(process.argv);
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const { program } = require('commander');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const fs = require('fs-extra');
|
|
6
|
+
const { startServer } = require('../src/server');
|
|
7
|
+
const open = (...args) => import('open').then(m => m.default(...args));
|
|
8
|
+
|
|
9
|
+
program
|
|
10
|
+
.version('0.1.0')
|
|
11
|
+
.description('Translation Manager CLI - Manage your React translations with a modern UI')
|
|
12
|
+
.option('-p, --port <number>', 'Port to run the UI on', 3000)
|
|
13
|
+
.option('-c, --config <path>', 'Path to config file')
|
|
14
|
+
.action(async (options) => {
|
|
15
|
+
const targetDir = process.cwd();
|
|
16
|
+
let config = {};
|
|
17
|
+
|
|
18
|
+
// Load config from file
|
|
19
|
+
const configPath = options.config || 'translation.config.json';
|
|
20
|
+
const absoluteConfigPath = path.resolve(targetDir, configPath);
|
|
21
|
+
|
|
22
|
+
if (await fs.pathExists(absoluteConfigPath)) {
|
|
23
|
+
config = await fs.readJson(absoluteConfigPath);
|
|
24
|
+
} else {
|
|
25
|
+
// Check for JS config
|
|
26
|
+
const jsConfigPath = path.resolve(targetDir, 'translation.config.js');
|
|
27
|
+
if (await fs.pathExists(jsConfigPath)) {
|
|
28
|
+
config = require(jsConfigPath);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
console.log('\x1b[36mโน\x1b[0m Starting Translation Manager...');
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
startServer(targetDir, options.port, config);
|
|
36
|
+
|
|
37
|
+
// Open the browser
|
|
38
|
+
await open(`http://localhost:${options.port}`);
|
|
39
|
+
} catch (err) {
|
|
40
|
+
console.error('\x1b[31mโ\x1b[0m Error:', err.message);
|
|
41
|
+
process.exit(1);
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
program.parse(process.argv);
|
package/package.json
CHANGED
|
@@ -1,66 +1,66 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@tuhama/translation-manager",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "A modern, web-based UI for managing i18n translation files in React and other JavaScript projects.",
|
|
5
|
-
"author": "Tuhama <tuhama.gh.qlyshi@gmail.com>",
|
|
6
|
-
"license": "MIT",
|
|
7
|
-
"engines": {
|
|
8
|
-
"node": ">=18"
|
|
9
|
-
},
|
|
10
|
-
"publishConfig": {
|
|
11
|
-
"access": "public"
|
|
12
|
-
},
|
|
13
|
-
"main": "src/server.js",
|
|
14
|
-
"bin": {
|
|
15
|
-
"translation-manager": "bin/index.js"
|
|
16
|
-
},
|
|
17
|
-
"files": [
|
|
18
|
-
"bin",
|
|
19
|
-
"src",
|
|
20
|
-
"web/dist",
|
|
21
|
-
"README.md",
|
|
22
|
-
"package.json"
|
|
23
|
-
],
|
|
24
|
-
"scripts": {
|
|
25
|
-
"dev": "concurrently \"npm run dev-server\" \"npm run dev-web\"",
|
|
26
|
-
"dev-server": "nodemon bin/index.js",
|
|
27
|
-
"dev-web": "cd web && npm run dev",
|
|
28
|
-
"build": "cd web && npm run build",
|
|
29
|
-
"start": "node bin/index.js"
|
|
30
|
-
},
|
|
31
|
-
"repository": {
|
|
32
|
-
"type": "git",
|
|
33
|
-
"url": "git+https://github.com/Tuhama/translationManager.git"
|
|
34
|
-
},
|
|
35
|
-
"keywords": [
|
|
36
|
-
"translation",
|
|
37
|
-
"i18n",
|
|
38
|
-
"localization",
|
|
39
|
-
"l10n",
|
|
40
|
-
"react",
|
|
41
|
-
"ui",
|
|
42
|
-
"cli",
|
|
43
|
-
"translation-manager",
|
|
44
|
-
"internationalization"
|
|
45
|
-
],
|
|
46
|
-
"bugs": {
|
|
47
|
-
"url": "https://github.com/Tuhama/translationManager/issues"
|
|
48
|
-
},
|
|
49
|
-
"homepage": "https://github.com/Tuhama/translationManager#readme",
|
|
50
|
-
"dependencies": {
|
|
51
|
-
"
|
|
52
|
-
"chokidar": "^5.0.0",
|
|
53
|
-
"commander": "^14.0.3",
|
|
54
|
-
"cors": "^2.8.6",
|
|
55
|
-
"express": "^5.2.1",
|
|
56
|
-
"express-history-api-fallback": "^2.2.1",
|
|
57
|
-
"fs-extra": "^11.3.4",
|
|
58
|
-
"lodash": "^4.17.23",
|
|
59
|
-
"open": "^11.0.0",
|
|
60
|
-
"picocolors": "^1.1.1"
|
|
61
|
-
},
|
|
62
|
-
"devDependencies": {
|
|
63
|
-
"concurrently": "^9.2.1",
|
|
64
|
-
"nodemon": "^3.1.14"
|
|
65
|
-
}
|
|
66
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@tuhama/translation-manager",
|
|
3
|
+
"version": "0.4.0",
|
|
4
|
+
"description": "A modern, web-based UI for managing i18n translation files in React and other JavaScript projects.",
|
|
5
|
+
"author": "Tuhama <tuhama.gh.qlyshi@gmail.com>",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"engines": {
|
|
8
|
+
"node": ">=18"
|
|
9
|
+
},
|
|
10
|
+
"publishConfig": {
|
|
11
|
+
"access": "public"
|
|
12
|
+
},
|
|
13
|
+
"main": "src/server.js",
|
|
14
|
+
"bin": {
|
|
15
|
+
"translation-manager": "bin/index.js"
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"bin",
|
|
19
|
+
"src",
|
|
20
|
+
"web/dist",
|
|
21
|
+
"README.md",
|
|
22
|
+
"package.json"
|
|
23
|
+
],
|
|
24
|
+
"scripts": {
|
|
25
|
+
"dev": "concurrently \"npm run dev-server\" \"npm run dev-web\"",
|
|
26
|
+
"dev-server": "nodemon bin/index.js",
|
|
27
|
+
"dev-web": "cd web && npm run dev",
|
|
28
|
+
"build": "cd web && npm run build",
|
|
29
|
+
"start": "node bin/index.js"
|
|
30
|
+
},
|
|
31
|
+
"repository": {
|
|
32
|
+
"type": "git",
|
|
33
|
+
"url": "git+https://github.com/Tuhama/translationManager.git"
|
|
34
|
+
},
|
|
35
|
+
"keywords": [
|
|
36
|
+
"translation",
|
|
37
|
+
"i18n",
|
|
38
|
+
"localization",
|
|
39
|
+
"l10n",
|
|
40
|
+
"react",
|
|
41
|
+
"ui",
|
|
42
|
+
"cli",
|
|
43
|
+
"translation-manager",
|
|
44
|
+
"internationalization"
|
|
45
|
+
],
|
|
46
|
+
"bugs": {
|
|
47
|
+
"url": "https://github.com/Tuhama/translationManager/issues"
|
|
48
|
+
},
|
|
49
|
+
"homepage": "https://github.com/Tuhama/translationManager#readme",
|
|
50
|
+
"dependencies": {
|
|
51
|
+
"@google-cloud/translate": "^8.0.0",
|
|
52
|
+
"chokidar": "^5.0.0",
|
|
53
|
+
"commander": "^14.0.3",
|
|
54
|
+
"cors": "^2.8.6",
|
|
55
|
+
"express": "^5.2.1",
|
|
56
|
+
"express-history-api-fallback": "^2.2.1",
|
|
57
|
+
"fs-extra": "^11.3.4",
|
|
58
|
+
"lodash": "^4.17.23",
|
|
59
|
+
"open": "^11.0.0",
|
|
60
|
+
"picocolors": "^1.1.1"
|
|
61
|
+
},
|
|
62
|
+
"devDependencies": {
|
|
63
|
+
"concurrently": "^9.2.1",
|
|
64
|
+
"nodemon": "^3.1.14"
|
|
65
|
+
}
|
|
66
|
+
}
|
package/src/core/Scanner.js
CHANGED
|
@@ -1,128 +1,128 @@
|
|
|
1
|
-
const fs = require('fs-extra');
|
|
2
|
-
const path = require('path');
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Scanner class to find translation key usages in source code.
|
|
6
|
-
*/
|
|
7
|
-
class Scanner {
|
|
8
|
-
constructor(targetDir, localesDir, config = {}) {
|
|
9
|
-
this.targetDir = targetDir;
|
|
10
|
-
this.localesDir = localesDir;
|
|
11
|
-
this.config = config;
|
|
12
|
-
this.extensions = config.extensions || ['.js', '.jsx', '.ts', '.tsx', '.html', '.vue'];
|
|
13
|
-
this.exclude = config.exclude || ['node_modules', '.git', 'dist', 'build', localesDir];
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Recursively gets files from the target directory.
|
|
18
|
-
*/
|
|
19
|
-
async getFiles() {
|
|
20
|
-
const files = [];
|
|
21
|
-
|
|
22
|
-
const walk = async (dir) => {
|
|
23
|
-
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
24
|
-
|
|
25
|
-
for (const entry of entries) {
|
|
26
|
-
const fullPath = path.resolve(dir, entry.name);
|
|
27
|
-
|
|
28
|
-
if (entry.isDirectory()) {
|
|
29
|
-
if (this.exclude.includes(entry.name) || this.exclude.some(ex => fullPath === path.resolve(this.targetDir, ex) || fullPath.startsWith(path.resolve(this.targetDir, ex) + path.sep))) {
|
|
30
|
-
continue;
|
|
31
|
-
}
|
|
32
|
-
await walk(fullPath);
|
|
33
|
-
} else if (this.extensions.includes(path.extname(fullPath))) {
|
|
34
|
-
files.push(fullPath);
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
await walk(this.targetDir);
|
|
40
|
-
return files;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Scans source code for key usages.
|
|
45
|
-
*/
|
|
46
|
-
async findUnusedKeys(allKeys) {
|
|
47
|
-
const files = await this.getFiles();
|
|
48
|
-
const contents = await Promise.all(files.map(f => fs.readFile(f, 'utf-8')));
|
|
49
|
-
const combinedContent = contents.join('\n---\n');
|
|
50
|
-
|
|
51
|
-
const used = new Set();
|
|
52
|
-
const maybeUsed = new Set();
|
|
53
|
-
const unused = [];
|
|
54
|
-
|
|
55
|
-
allKeys.forEach(key => {
|
|
56
|
-
// 1. Literal usage
|
|
57
|
-
const escapedKey = key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
58
|
-
const literalRegex = new RegExp(`['"\`]${escapedKey}['"\`]`, 'g');
|
|
59
|
-
|
|
60
|
-
if (literalRegex.test(combinedContent)) {
|
|
61
|
-
used.add(key);
|
|
62
|
-
return;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// 2. Dynamic usage
|
|
66
|
-
const parts = key.split('.');
|
|
67
|
-
let isMaybeUsed = false;
|
|
68
|
-
|
|
69
|
-
for (let i = 1; i < parts.length; i++) {
|
|
70
|
-
const prefix = parts.slice(0, i).join('.') + '.';
|
|
71
|
-
const escapedPrefix = prefix.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
72
|
-
const dynamicRegex = new RegExp(`(['"\`]${escapedPrefix}['"\`].*?[+])|(['"\`]${escapedPrefix}.*?\$\{)`, 'g');
|
|
73
|
-
|
|
74
|
-
if (dynamicRegex.test(combinedContent)) {
|
|
75
|
-
isMaybeUsed = true;
|
|
76
|
-
break;
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
if (isMaybeUsed) {
|
|
81
|
-
maybeUsed.add(key);
|
|
82
|
-
} else {
|
|
83
|
-
unused.push(key);
|
|
84
|
-
}
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
return {
|
|
88
|
-
unused: unused.sort(),
|
|
89
|
-
maybeUsed: Array.from(maybeUsed).sort()
|
|
90
|
-
};
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* Finds keys that are used in source code but missing from translation files.
|
|
95
|
-
*/
|
|
96
|
-
async findMissingKeys(existingKeys) {
|
|
97
|
-
const files = await this.getFiles();
|
|
98
|
-
const contents = await Promise.all(files.map(f => fs.readFile(f, 'utf-8')));
|
|
99
|
-
const combinedContent = contents.join('\n---\n');
|
|
100
|
-
|
|
101
|
-
const missingKeys = new Set();
|
|
102
|
-
const existingKeysSet = new Set(existingKeys);
|
|
103
|
-
|
|
104
|
-
// Regex patterns to find potential keys:
|
|
105
|
-
// 1. t('key')
|
|
106
|
-
// 2. i18n.t('key')
|
|
107
|
-
// 3. i18nKey="key"
|
|
108
|
-
// 4. <Trans i18nKey="key">
|
|
109
|
-
const patterns = [
|
|
110
|
-
/(?:\bt\(|i18n\.t\(|i18nKey=)\s*['"\`]([^'"\`]+)['"\`]/g
|
|
111
|
-
];
|
|
112
|
-
|
|
113
|
-
patterns.forEach(regex => {
|
|
114
|
-
let match;
|
|
115
|
-
while ((match = regex.exec(combinedContent)) !== null) {
|
|
116
|
-
const key = match[1];
|
|
117
|
-
// basic validation to avoid random strings
|
|
118
|
-
if (key && key.includes('.') && !existingKeysSet.has(key)) {
|
|
119
|
-
missingKeys.add(key);
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
return Array.from(missingKeys).sort();
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
module.exports = Scanner;
|
|
1
|
+
const fs = require('fs-extra');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Scanner class to find translation key usages in source code.
|
|
6
|
+
*/
|
|
7
|
+
class Scanner {
|
|
8
|
+
constructor(targetDir, localesDir, config = {}) {
|
|
9
|
+
this.targetDir = targetDir;
|
|
10
|
+
this.localesDir = localesDir;
|
|
11
|
+
this.config = config;
|
|
12
|
+
this.extensions = config.extensions || ['.js', '.jsx', '.ts', '.tsx', '.html', '.vue'];
|
|
13
|
+
this.exclude = config.exclude || ['node_modules', '.git', 'dist', 'build', localesDir];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Recursively gets files from the target directory.
|
|
18
|
+
*/
|
|
19
|
+
async getFiles() {
|
|
20
|
+
const files = [];
|
|
21
|
+
|
|
22
|
+
const walk = async (dir) => {
|
|
23
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
24
|
+
|
|
25
|
+
for (const entry of entries) {
|
|
26
|
+
const fullPath = path.resolve(dir, entry.name);
|
|
27
|
+
|
|
28
|
+
if (entry.isDirectory()) {
|
|
29
|
+
if (this.exclude.includes(entry.name) || this.exclude.some(ex => fullPath === path.resolve(this.targetDir, ex) || fullPath.startsWith(path.resolve(this.targetDir, ex) + path.sep))) {
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
await walk(fullPath);
|
|
33
|
+
} else if (this.extensions.includes(path.extname(fullPath))) {
|
|
34
|
+
files.push(fullPath);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
await walk(this.targetDir);
|
|
40
|
+
return files;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Scans source code for key usages.
|
|
45
|
+
*/
|
|
46
|
+
async findUnusedKeys(allKeys) {
|
|
47
|
+
const files = await this.getFiles();
|
|
48
|
+
const contents = await Promise.all(files.map(f => fs.readFile(f, 'utf-8')));
|
|
49
|
+
const combinedContent = contents.join('\n---\n');
|
|
50
|
+
|
|
51
|
+
const used = new Set();
|
|
52
|
+
const maybeUsed = new Set();
|
|
53
|
+
const unused = [];
|
|
54
|
+
|
|
55
|
+
allKeys.forEach(key => {
|
|
56
|
+
// 1. Literal usage
|
|
57
|
+
const escapedKey = key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
58
|
+
const literalRegex = new RegExp(`['"\`]${escapedKey}['"\`]`, 'g');
|
|
59
|
+
|
|
60
|
+
if (literalRegex.test(combinedContent)) {
|
|
61
|
+
used.add(key);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// 2. Dynamic usage
|
|
66
|
+
const parts = key.split('.');
|
|
67
|
+
let isMaybeUsed = false;
|
|
68
|
+
|
|
69
|
+
for (let i = 1; i < parts.length; i++) {
|
|
70
|
+
const prefix = parts.slice(0, i).join('.') + '.';
|
|
71
|
+
const escapedPrefix = prefix.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
72
|
+
const dynamicRegex = new RegExp(`(['"\`]${escapedPrefix}['"\`].*?[+])|(['"\`]${escapedPrefix}.*?\$\{)`, 'g');
|
|
73
|
+
|
|
74
|
+
if (dynamicRegex.test(combinedContent)) {
|
|
75
|
+
isMaybeUsed = true;
|
|
76
|
+
break;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (isMaybeUsed) {
|
|
81
|
+
maybeUsed.add(key);
|
|
82
|
+
} else {
|
|
83
|
+
unused.push(key);
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
unused: unused.sort(),
|
|
89
|
+
maybeUsed: Array.from(maybeUsed).sort()
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Finds keys that are used in source code but missing from translation files.
|
|
95
|
+
*/
|
|
96
|
+
async findMissingKeys(existingKeys) {
|
|
97
|
+
const files = await this.getFiles();
|
|
98
|
+
const contents = await Promise.all(files.map(f => fs.readFile(f, 'utf-8')));
|
|
99
|
+
const combinedContent = contents.join('\n---\n');
|
|
100
|
+
|
|
101
|
+
const missingKeys = new Set();
|
|
102
|
+
const existingKeysSet = new Set(existingKeys);
|
|
103
|
+
|
|
104
|
+
// Regex patterns to find potential keys:
|
|
105
|
+
// 1. t('key')
|
|
106
|
+
// 2. i18n.t('key')
|
|
107
|
+
// 3. i18nKey="key"
|
|
108
|
+
// 4. <Trans i18nKey="key">
|
|
109
|
+
const patterns = [
|
|
110
|
+
/(?:\bt\(|i18n\.t\(|i18nKey=)\s*['"\`]([^'"\`]+)['"\`]/g
|
|
111
|
+
];
|
|
112
|
+
|
|
113
|
+
patterns.forEach(regex => {
|
|
114
|
+
let match;
|
|
115
|
+
while ((match = regex.exec(combinedContent)) !== null) {
|
|
116
|
+
const key = match[1];
|
|
117
|
+
// basic validation to avoid random strings
|
|
118
|
+
if (key && key.includes('.') && !existingKeysSet.has(key)) {
|
|
119
|
+
missingKeys.add(key);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
return Array.from(missingKeys).sort();
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
module.exports = Scanner;
|