react-i18next-scanner 0.1.0 → 0.1.2
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 +81 -5
- package/dist/cli/cli.js +13 -1
- package/dist/config/loadConfig.js +4 -0
- package/dist/core/actions/removeUnusedTranslations.js +2 -2
- package/dist/core/actions/syncMissingTranslations.js +2 -2
- package/dist/core/actions/writeDynamicTranslationReport.js +14 -0
- package/dist/core/analysis/detectDynamicTranslationUsages.js +94 -0
- package/dist/core/runInit.js +58 -0
- package/dist/core/runSyncMissing.js +1 -1
- package/dist/core/scanner.js +6 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@ CLI tool to scan React projects for missing and unused `react-i18next` translati
|
|
|
11
11
|
- Sync missing keys into translation JSON files
|
|
12
12
|
- Remove unused keys from translation JSON files
|
|
13
13
|
- Sort translation JSON files alphabetically
|
|
14
|
-
-
|
|
14
|
+
- Detect dynamic translation usages and write them into a separate report
|
|
15
15
|
|
|
16
16
|
---
|
|
17
17
|
|
|
@@ -37,6 +37,8 @@ Create one of these config files in your project root:
|
|
|
37
37
|
- `i18n-scanner.config.cjs`
|
|
38
38
|
- `i18n-scanner.config.mjs`
|
|
39
39
|
|
|
40
|
+
> `.ts` config is not supported yet.
|
|
41
|
+
|
|
40
42
|
Example:
|
|
41
43
|
|
|
42
44
|
```js
|
|
@@ -112,6 +114,20 @@ If `true`, unused keys will be removed from translation JSON files.
|
|
|
112
114
|
|
|
113
115
|
## Commands
|
|
114
116
|
|
|
117
|
+
### Initialize config
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
npx react-i18next-scanner init
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
This command creates a config file automatically and lets you choose:
|
|
124
|
+
|
|
125
|
+
- `.js`
|
|
126
|
+
- `.mjs`
|
|
127
|
+
- `.cjs`
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
115
131
|
### Scan project
|
|
116
132
|
|
|
117
133
|
```bash
|
|
@@ -130,6 +146,7 @@ What it does:
|
|
|
130
146
|
- extracts translation keys
|
|
131
147
|
- compares them with translation JSON files
|
|
132
148
|
- writes unused translation reports
|
|
149
|
+
- writes dynamic translation usage reports
|
|
133
150
|
- optionally syncs missing keys
|
|
134
151
|
- optionally removes unused keys
|
|
135
152
|
|
|
@@ -178,7 +195,40 @@ These files contain only unused translations, so you can review them safely befo
|
|
|
178
195
|
|
|
179
196
|
---
|
|
180
197
|
|
|
181
|
-
##
|
|
198
|
+
## Dynamic Translation Report
|
|
199
|
+
|
|
200
|
+
If the scanner finds dynamic translation usage such as:
|
|
201
|
+
|
|
202
|
+
```ts
|
|
203
|
+
t(getDescription());
|
|
204
|
+
t(labelKey);
|
|
205
|
+
t(transaction.descriptionKey);
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
it creates a report file:
|
|
209
|
+
|
|
210
|
+
```bash
|
|
211
|
+
reports-dynamic/dynamic-translation-usages.json
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
Example structure:
|
|
215
|
+
|
|
216
|
+
```json
|
|
217
|
+
{
|
|
218
|
+
"src/views/Account/BalancesTransactionsPage/PaymentDescription.tsx": [
|
|
219
|
+
{
|
|
220
|
+
"namespaces": ["accountInformation"],
|
|
221
|
+
"expression": "getDescription()",
|
|
222
|
+
"reason": "Object-based non-literal translation key",
|
|
223
|
+
"suggestion": "Translation key may come from imported object(s) or helper functions. Manual or deeper analysis may be required."
|
|
224
|
+
}
|
|
225
|
+
]
|
|
226
|
+
}
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
---
|
|
230
|
+
|
|
231
|
+
## Supported Translation Patterns
|
|
182
232
|
|
|
183
233
|
The scanner currently detects translation keys used with:
|
|
184
234
|
|
|
@@ -187,6 +237,31 @@ The scanner currently detects translation keys used with:
|
|
|
187
237
|
- `<Trans i18nKey="key" />`
|
|
188
238
|
- `useTranslation("namespace")`
|
|
189
239
|
|
|
240
|
+
It can also report dynamic usages where translation keys are not passed directly as string literals.
|
|
241
|
+
|
|
242
|
+
---
|
|
243
|
+
|
|
244
|
+
## Supported Files
|
|
245
|
+
|
|
246
|
+
### Source files
|
|
247
|
+
|
|
248
|
+
The scanner can scan:
|
|
249
|
+
|
|
250
|
+
- `.js`
|
|
251
|
+
- `.jsx`
|
|
252
|
+
- `.ts`
|
|
253
|
+
- `.tsx`
|
|
254
|
+
|
|
255
|
+
### Config files
|
|
256
|
+
|
|
257
|
+
The scanner currently supports:
|
|
258
|
+
|
|
259
|
+
- `.js`
|
|
260
|
+
- `.cjs`
|
|
261
|
+
- `.mjs`
|
|
262
|
+
|
|
263
|
+
> `.ts` config is not supported yet.
|
|
264
|
+
|
|
190
265
|
---
|
|
191
266
|
|
|
192
267
|
## Example Workflow
|
|
@@ -252,9 +327,10 @@ npx react-i18next-scanner scan
|
|
|
252
327
|
|
|
253
328
|
## Notes
|
|
254
329
|
|
|
255
|
-
-
|
|
256
|
-
-
|
|
257
|
-
-
|
|
330
|
+
- `syncMissing` does not translate values automatically. It only adds missing keys with empty values.
|
|
331
|
+
- `removeUnused` should be used carefully after reviewing unused translation reports.
|
|
332
|
+
- Dynamic translation usages are written into a separate report and are not automatically resolved.
|
|
333
|
+
- Config paths are resolved relative to the config file location.
|
|
258
334
|
|
|
259
335
|
---
|
|
260
336
|
|
package/dist/cli/cli.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { Command } from "commander";
|
|
3
3
|
import chalk from "chalk";
|
|
4
|
+
import { runInit } from "../core/runInit.js";
|
|
4
5
|
import { runScanner } from "../core/scanner.js";
|
|
5
6
|
import { sortJsonFiles } from "../core/sortJsonFiles.js";
|
|
6
7
|
import { runSyncMissing } from "../core/runSyncMissing.js";
|
|
@@ -13,7 +14,7 @@ function resolveConfigPath(configPath) {
|
|
|
13
14
|
finalConfigPath = findDefaultConfig() ?? undefined;
|
|
14
15
|
}
|
|
15
16
|
if (!finalConfigPath) {
|
|
16
|
-
throw new Error(`Config file not found. Create one of: ${POSSIBLE_CONFIGS.join(", ")}`);
|
|
17
|
+
throw new Error(`Config file not found. Create one of: ${POSSIBLE_CONFIGS.join(", ")}\nRun: react-i18next-scanner init`);
|
|
17
18
|
}
|
|
18
19
|
return finalConfigPath;
|
|
19
20
|
}
|
|
@@ -96,4 +97,15 @@ program
|
|
|
96
97
|
handleCliError(error);
|
|
97
98
|
}
|
|
98
99
|
});
|
|
100
|
+
program
|
|
101
|
+
.command("init")
|
|
102
|
+
.description("Create scanner config file automatically")
|
|
103
|
+
.action(async () => {
|
|
104
|
+
try {
|
|
105
|
+
await runInit();
|
|
106
|
+
}
|
|
107
|
+
catch (error) {
|
|
108
|
+
handleCliError(error);
|
|
109
|
+
}
|
|
110
|
+
});
|
|
99
111
|
await program.parseAsync(process.argv);
|
|
@@ -41,6 +41,9 @@ function validateConfig(config) {
|
|
|
41
41
|
if (cfg.syncMissing !== undefined && typeof cfg.syncMissing !== "boolean") {
|
|
42
42
|
throw new Error("syncMissing must be a boolean.");
|
|
43
43
|
}
|
|
44
|
+
if (cfg.sortJson !== undefined && typeof cfg.sortJson !== "boolean") {
|
|
45
|
+
throw new Error("sortJson must be a boolean.");
|
|
46
|
+
}
|
|
44
47
|
if (cfg.outputDir !== undefined && typeof cfg.outputDir !== "string") {
|
|
45
48
|
throw new Error("outputDir must be a string.");
|
|
46
49
|
}
|
|
@@ -52,6 +55,7 @@ function normalizeConfig(config, baseDir) {
|
|
|
52
55
|
sortJsonFiles: config.sortJsonFiles?.map((p) => path.resolve(baseDir, p)),
|
|
53
56
|
removeUnused: config.removeUnused ?? false,
|
|
54
57
|
syncMissing: config.syncMissing ?? false,
|
|
58
|
+
sortJson: config.sortJson ?? false,
|
|
55
59
|
outputDir: path.resolve(baseDir, config.outputDir ?? "unused-translations"),
|
|
56
60
|
};
|
|
57
61
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from "fs/promises";
|
|
2
2
|
import { removeKey } from "../utils/removeKey.js";
|
|
3
3
|
import { sortObject } from "../utils/sortObject.js";
|
|
4
|
-
export async function removeUnusedTranslations(result, jsonFiles) {
|
|
4
|
+
export async function removeUnusedTranslations(result, jsonFiles, sortJson) {
|
|
5
5
|
const { unusedKeysByFile } = result;
|
|
6
6
|
for (const file of jsonFiles) {
|
|
7
7
|
const unusedKeys = unusedKeysByFile[file.filePath] ?? [];
|
|
@@ -19,7 +19,7 @@ export async function removeUnusedTranslations(result, jsonFiles) {
|
|
|
19
19
|
if (!updated) {
|
|
20
20
|
continue;
|
|
21
21
|
}
|
|
22
|
-
const sorted = sortObject(file.data);
|
|
22
|
+
const sorted = sortJson ? sortObject(file.data) : file.data;
|
|
23
23
|
file.data = sorted;
|
|
24
24
|
await fs.writeFile(file.filePath, JSON.stringify(sorted, null, 2), "utf-8");
|
|
25
25
|
console.log(`\n✅ Unused keys removed: ${file.filePath}`);
|
|
@@ -2,7 +2,7 @@ import fs from "fs/promises";
|
|
|
2
2
|
import { addKey } from "../utils/addKey.js";
|
|
3
3
|
import { sortObject } from "../utils/sortObject.js";
|
|
4
4
|
import { isValidTranslationKey } from "../utils/isValidTranslationKey.js";
|
|
5
|
-
export async function syncMissingTranslations(result, jsonFiles) {
|
|
5
|
+
export async function syncMissingTranslations(result, jsonFiles, sortJson) {
|
|
6
6
|
const { missingKeysByFile } = result;
|
|
7
7
|
for (const file of jsonFiles) {
|
|
8
8
|
const missingKeys = missingKeysByFile[file.filePath] ?? [];
|
|
@@ -23,7 +23,7 @@ export async function syncMissingTranslations(result, jsonFiles) {
|
|
|
23
23
|
if (!updated) {
|
|
24
24
|
continue;
|
|
25
25
|
}
|
|
26
|
-
const sorted = sortObject(file.data);
|
|
26
|
+
const sorted = sortJson ? sortObject(file.data) : file.data;
|
|
27
27
|
file.data = sorted;
|
|
28
28
|
await fs.writeFile(file.filePath, JSON.stringify(sorted, null, 2), "utf-8");
|
|
29
29
|
console.log(`\n✅ Missing keys synced: ${file.filePath}`);
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import fs from "fs/promises";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { sortObject } from "../utils/sortObject.js";
|
|
4
|
+
const DEFAULT_DYNAMIC_REPORT_DIR = path.resolve(process.cwd(), "reports-dynamic");
|
|
5
|
+
export async function writeDynamicTranslationReport(report, outputDir = DEFAULT_DYNAMIC_REPORT_DIR) {
|
|
6
|
+
if (Object.keys(report).length === 0) {
|
|
7
|
+
return;
|
|
8
|
+
}
|
|
9
|
+
await fs.mkdir(outputDir, { recursive: true });
|
|
10
|
+
const reportPath = path.join(outputDir, "dynamic-translation-usages.json");
|
|
11
|
+
const sortedReport = sortObject(report);
|
|
12
|
+
await fs.writeFile(reportPath, `${JSON.stringify(sortedReport, null, 2)}\n`, "utf-8");
|
|
13
|
+
console.log(`📝 Dynamic translation report created: ${reportPath}`);
|
|
14
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import fs from "fs/promises";
|
|
2
|
+
import path from "path";
|
|
3
|
+
function extractNamespaces(content) {
|
|
4
|
+
const namespaces = new Set();
|
|
5
|
+
const nsRegex = /useTranslation\s*\(\s*(?:\[\s*([^\]]+)\s*\]|['"`]([^'"`]+)['"`])?/g;
|
|
6
|
+
let match;
|
|
7
|
+
while ((match = nsRegex.exec(content)) !== null) {
|
|
8
|
+
if (match[1]) {
|
|
9
|
+
const values = match[1]
|
|
10
|
+
.split(",")
|
|
11
|
+
.map((value) => value.replace(/['"`]/g, "").trim())
|
|
12
|
+
.filter(Boolean);
|
|
13
|
+
values.forEach((value) => namespaces.add(value));
|
|
14
|
+
}
|
|
15
|
+
if (match[2]) {
|
|
16
|
+
namespaces.add(match[2]);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return [...namespaces];
|
|
20
|
+
}
|
|
21
|
+
function extractDynamicTExpressions(content) {
|
|
22
|
+
const expressions = new Set();
|
|
23
|
+
const dynamicTRegex = /(^|[^\w$.])t\s*\(\s*([A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*(?:\([^()]*\))?)/g;
|
|
24
|
+
let match;
|
|
25
|
+
while ((match = dynamicTRegex.exec(content)) !== null) {
|
|
26
|
+
const expression = match[2]?.trim();
|
|
27
|
+
if (!expression) {
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
expressions.add(expression);
|
|
31
|
+
}
|
|
32
|
+
return [...expressions];
|
|
33
|
+
}
|
|
34
|
+
function extractImportedIdentifiers(content) {
|
|
35
|
+
const identifiers = new Set();
|
|
36
|
+
const importRegex = /import\s+(?:\{([^}]+)\}|([A-Za-z_$][\w$]*))\s+from\s+['"`][^'"`]+['"`]/g;
|
|
37
|
+
let match;
|
|
38
|
+
while ((match = importRegex.exec(content)) !== null) {
|
|
39
|
+
if (match[1]) {
|
|
40
|
+
const namedImports = match[1]
|
|
41
|
+
.split(",")
|
|
42
|
+
.map((value) => value.trim())
|
|
43
|
+
.map((value) => value.split(" as ")[1]?.trim() ?? value.split(" as ")[0]?.trim())
|
|
44
|
+
.filter(Boolean);
|
|
45
|
+
namedImports.forEach((value) => identifiers.add(value));
|
|
46
|
+
}
|
|
47
|
+
if (match[2]) {
|
|
48
|
+
identifiers.add(match[2]);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return [...identifiers];
|
|
52
|
+
}
|
|
53
|
+
function detectObjectBasedUsageHints(content, importedIdentifiers) {
|
|
54
|
+
const hints = new Set();
|
|
55
|
+
for (const identifier of importedIdentifiers) {
|
|
56
|
+
const objectAccessRegex = new RegExp(`${identifier}\\s*\\[[^\\]]+\\]\\.(description|label|title|placeholder|message)|${identifier}\\.[A-Za-z_$][\\w$]*\\.(description|label|title|placeholder|message)`, "g");
|
|
57
|
+
if (objectAccessRegex.test(content)) {
|
|
58
|
+
hints.add(identifier);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return [...hints];
|
|
62
|
+
}
|
|
63
|
+
function buildFinding(namespaces, expression, objectBasedHints) {
|
|
64
|
+
const isObjectBased = objectBasedHints.length > 0;
|
|
65
|
+
return {
|
|
66
|
+
namespaces,
|
|
67
|
+
expression,
|
|
68
|
+
reason: isObjectBased
|
|
69
|
+
? "Object-based non-literal translation key"
|
|
70
|
+
: "Non-literal translation key",
|
|
71
|
+
suggestion: isObjectBased
|
|
72
|
+
? `Translation key may come from imported object(s): ${objectBasedHints.join(", ")}. Manual or deeper analysis may be required.`
|
|
73
|
+
: "Translation key may come from a variable or helper function. Manual or deeper analysis may be required.",
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
export async function detectDynamicTranslationUsages(files) {
|
|
77
|
+
const report = {};
|
|
78
|
+
for (const file of files) {
|
|
79
|
+
const content = await fs.readFile(file, "utf-8");
|
|
80
|
+
if (!content.includes("useTranslation") || !content.includes("t(")) {
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
const namespaces = extractNamespaces(content);
|
|
84
|
+
const expressions = extractDynamicTExpressions(content);
|
|
85
|
+
if (expressions.length === 0) {
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
const importedIdentifiers = extractImportedIdentifiers(content);
|
|
89
|
+
const objectBasedHints = detectObjectBasedUsageHints(content, importedIdentifiers);
|
|
90
|
+
const relativePath = path.relative(process.cwd(), file).replace(/\\/g, "/");
|
|
91
|
+
report[relativePath] = expressions.map((expression) => buildFinding(namespaces, expression, objectBasedHints));
|
|
92
|
+
}
|
|
93
|
+
return report;
|
|
94
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import fs from "fs/promises";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import readline from "readline/promises";
|
|
4
|
+
import { stdin as input, stdout as output } from "process";
|
|
5
|
+
function getConfigFileName(format) {
|
|
6
|
+
return `i18n-scanner.config.${format}`;
|
|
7
|
+
}
|
|
8
|
+
function getConfigTemplate(format) {
|
|
9
|
+
const configObject = `{
|
|
10
|
+
srcPaths: ["./src/**/*.{js,jsx,ts,tsx}"],
|
|
11
|
+
jsonDir: [
|
|
12
|
+
"./src/i18n/locales/translations.en.json",
|
|
13
|
+
"./src/i18n/locales/translations.es.json"
|
|
14
|
+
],
|
|
15
|
+
sortJsonFiles: [
|
|
16
|
+
"./src/i18n/locales/translations.en.json",
|
|
17
|
+
"./src/i18n/locales/translations.es.json"
|
|
18
|
+
],
|
|
19
|
+
outputDir: "./unused",
|
|
20
|
+
syncMissing: false,
|
|
21
|
+
removeUnused: false
|
|
22
|
+
}`;
|
|
23
|
+
if (format === "cjs") {
|
|
24
|
+
return `module.exports = ${configObject};\n`;
|
|
25
|
+
}
|
|
26
|
+
return `export default ${configObject};\n`;
|
|
27
|
+
}
|
|
28
|
+
export async function runInit() {
|
|
29
|
+
const rl = readline.createInterface({ input, output });
|
|
30
|
+
try {
|
|
31
|
+
const answer = await rl.question("Choose config format (js / mjs / cjs): ");
|
|
32
|
+
const normalized = answer.trim().toLowerCase();
|
|
33
|
+
if (normalized !== "js" && normalized !== "mjs" && normalized !== "cjs") {
|
|
34
|
+
throw new Error("Invalid format. Use js, mjs, or cjs.");
|
|
35
|
+
}
|
|
36
|
+
const format = normalized;
|
|
37
|
+
const fileName = getConfigFileName(format);
|
|
38
|
+
const filePath = path.resolve(process.cwd(), fileName);
|
|
39
|
+
try {
|
|
40
|
+
await fs.access(filePath);
|
|
41
|
+
throw new Error(`${fileName} already exists.`);
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
if (error instanceof Error && !error.message.includes("already exists")) {
|
|
45
|
+
// file does not exist, continue
|
|
46
|
+
}
|
|
47
|
+
else if (error instanceof Error) {
|
|
48
|
+
throw error;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
const content = getConfigTemplate(format);
|
|
52
|
+
await fs.writeFile(filePath, content, "utf-8");
|
|
53
|
+
console.log(`✅ Created: ${fileName}`);
|
|
54
|
+
}
|
|
55
|
+
finally {
|
|
56
|
+
rl.close();
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -9,6 +9,6 @@ export async function runSyncMissing(config) {
|
|
|
9
9
|
const usedKeys = await extractKeysFromFiles(files);
|
|
10
10
|
const jsonData = await loadJsonKeys(config.jsonDir);
|
|
11
11
|
const result = analyzeKeys(usedKeys, jsonData);
|
|
12
|
-
await syncMissingTranslations(result, jsonData);
|
|
12
|
+
await syncMissingTranslations(result, jsonData, config.sortJson);
|
|
13
13
|
return result;
|
|
14
14
|
}
|
package/dist/core/scanner.js
CHANGED
|
@@ -2,20 +2,24 @@ import { loadJsonKeys } from "./io/jsonLoader.js";
|
|
|
2
2
|
import { getFiles } from "./io/getFiles.js";
|
|
3
3
|
import { analyzeKeys } from "./analysis/analyzer.js";
|
|
4
4
|
import { extractKeysFromFiles } from "./analysis/extractKeysFromFiles.js";
|
|
5
|
+
import { detectDynamicTranslationUsages } from "./analysis/detectDynamicTranslationUsages.js";
|
|
6
|
+
import { writeDynamicTranslationReport } from "./actions/writeDynamicTranslationReport.js";
|
|
5
7
|
import { syncMissingTranslations } from "./actions/syncMissingTranslations.js";
|
|
6
8
|
import { removeUnusedTranslations } from "./actions/removeUnusedTranslations.js";
|
|
7
9
|
import { writeUnusedTranslationsReport } from "./actions/writeUnusedTranslationsReport.js";
|
|
8
10
|
export async function runScanner(config) {
|
|
9
11
|
const files = await getFiles(config.srcPaths);
|
|
12
|
+
const dynamicUsages = await detectDynamicTranslationUsages(files);
|
|
13
|
+
await writeDynamicTranslationReport(dynamicUsages);
|
|
10
14
|
const usedKeys = await extractKeysFromFiles(files);
|
|
11
15
|
const jsonData = await loadJsonKeys(config.jsonDir);
|
|
12
16
|
const result = analyzeKeys(usedKeys, jsonData);
|
|
13
17
|
await writeUnusedTranslationsReport(result, jsonData, config.outputDir);
|
|
14
18
|
if (config.syncMissing) {
|
|
15
|
-
await syncMissingTranslations(result, jsonData);
|
|
19
|
+
await syncMissingTranslations(result, jsonData, config.sortJson);
|
|
16
20
|
}
|
|
17
21
|
if (config.removeUnused) {
|
|
18
|
-
await removeUnusedTranslations(result, jsonData);
|
|
22
|
+
await removeUnusedTranslations(result, jsonData, config.sortJson);
|
|
19
23
|
}
|
|
20
24
|
return result;
|
|
21
25
|
}
|
package/package.json
CHANGED