@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 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
- Specify a Google Translate API Key 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
- ### ๐Ÿช„ Normalization
37
- To keep your translation files organized, use the "**Normalize**" button to synchronize keys across all files and sort them alphabetically.
38
-
39
- ## Configuration
40
- You can optionally create a `translation.config.json` in your project root:
41
- ```json
42
- {
43
- "path": "src/locales",
44
- "port": 5000
45
- }
46
- ```
47
-
48
- ## Development
49
- To work on this repo:
50
- 1. `npm install`
51
- 2. `cd web && npm install`
52
- 3. `npm run dev` (starts both the API and the Vite UI)
53
-
54
- ## Limitations
55
- - **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.
56
- - **Namespaces**: Currently optimized for single-namespace or default-namespace projects.
57
-
58
- ## License
59
- MIT ยฉ [Tuhama](mailto:tuhama.gh.qlyshi@gmail.com)
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.2.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
- "axios": "^1.14.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
- }
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
+ }
@@ -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;