@drone1/alt 1.1.1 → 1.1.3
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/.claude/settings.local.json +8 -0
- package/package.json +1 -2
- package/src/commands/prune.js +156 -0
- package/src/commands/translate.js +5 -0
- package/src/main.mjs +22 -0
- package/test/common.mjs +1 -1
- package/test/prune-command.test.js +320 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@drone1/alt",
|
|
4
|
-
"version": "1.1.
|
|
4
|
+
"version": "1.1.3",
|
|
5
5
|
"description": "An AI-powered localization tool",
|
|
6
6
|
"main": "src/index.mjs",
|
|
7
7
|
"bin": {
|
|
@@ -10,7 +10,6 @@
|
|
|
10
10
|
"scripts": {
|
|
11
11
|
"test": "ALT_TEST=1 mocha",
|
|
12
12
|
"test:targeted": "ALT_TEST=1 mocha --grep 'multiple target'",
|
|
13
|
-
"test": "ALT_TEST=1 mocha",
|
|
14
13
|
"test:coverage": "ALT_TEST=1 nyc mocha",
|
|
15
14
|
"localize-display-strings": "./alt.mjs",
|
|
16
15
|
"print-all-help": "rm -f help.txt && (./alt.mjs help && echo -e '\n---\n' && ./alt.mjs help translate && echo -e '\n---\n' && ./alt.mjs help list-models) > help.txt",
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import * as path from 'path'
|
|
2
|
+
import { localize, localizeFormatted } from '../localizer/localize.js'
|
|
3
|
+
import { readJsonFile, writeJsonFile, normalizeOutputPath } from '../lib/io.js'
|
|
4
|
+
import { loadConfig } from '../lib/config.js'
|
|
5
|
+
import { loadReferenceFile } from '../lib/reference-loader.js'
|
|
6
|
+
import { assertIsObj } from '../lib/assert.js'
|
|
7
|
+
import { shutdown } from '../shutdown.js'
|
|
8
|
+
|
|
9
|
+
export async function runPrune({ appState, options, log }) {
|
|
10
|
+
let exitCode = 0
|
|
11
|
+
try {
|
|
12
|
+
// Load config
|
|
13
|
+
const config = await loadConfig({
|
|
14
|
+
configFile: options.configFile,
|
|
15
|
+
log
|
|
16
|
+
})
|
|
17
|
+
assertIsObj(config)
|
|
18
|
+
|
|
19
|
+
const referenceFile = options.referenceFile ?? config.referenceFile
|
|
20
|
+
if (!referenceFile?.length) {
|
|
21
|
+
throw new Error(
|
|
22
|
+
localize({
|
|
23
|
+
token: 'error-no-reference-file-specified',
|
|
24
|
+
lang: appState.lang,
|
|
25
|
+
log
|
|
26
|
+
})
|
|
27
|
+
)
|
|
28
|
+
}
|
|
29
|
+
log.D(`referenceFile=${referenceFile}`)
|
|
30
|
+
|
|
31
|
+
// Load reference file
|
|
32
|
+
const refFileDir = path.dirname(referenceFile)
|
|
33
|
+
const outputDir = path.resolve(options.outputDir ?? config.outputDir ?? refFileDir)
|
|
34
|
+
log.D(`outputDir=${outputDir}`)
|
|
35
|
+
|
|
36
|
+
// Create a tmp dir for storing the .mjs reference file
|
|
37
|
+
const { mkTmpDir } = await import('../lib/io.js')
|
|
38
|
+
const tmpDir = await mkTmpDir()
|
|
39
|
+
appState.tmpDir = tmpDir
|
|
40
|
+
|
|
41
|
+
// Resolve referenceExportedVarName
|
|
42
|
+
let referenceExportedVarName
|
|
43
|
+
const { getFileExtension } = await import('../lib/utils.js')
|
|
44
|
+
const referenceFileExt = getFileExtension(referenceFile)
|
|
45
|
+
if (['js','mjs'].includes(referenceFileExt)) {
|
|
46
|
+
log.D(`Searching for reference exported var name for .${referenceFileExt} extension...`)
|
|
47
|
+
if (options.referenceExportedVarName?.length) {
|
|
48
|
+
log.D(`Found reference exported var name via --reference-exported-var-name`)
|
|
49
|
+
referenceExportedVarName = options.referenceExportedVarName
|
|
50
|
+
} else if (config.referenceExportedVarName?.length) {
|
|
51
|
+
log.D(`Found reference exported var name in config, via 'referenceExportedVarName'`)
|
|
52
|
+
referenceExportedVarName = config.referenceExportedVarName
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
log.D(`referenceExportedVarName=${referenceExportedVarName}`)
|
|
56
|
+
|
|
57
|
+
// Load reference data
|
|
58
|
+
const referenceData = await loadReferenceFile({
|
|
59
|
+
appLang: appState.lang,
|
|
60
|
+
referenceFile,
|
|
61
|
+
referenceExportedVarName,
|
|
62
|
+
tmpDir,
|
|
63
|
+
log
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
if (!referenceData) {
|
|
67
|
+
throw new Error(
|
|
68
|
+
localizeFormatted({
|
|
69
|
+
token: 'error-no-reference-data-in-variable',
|
|
70
|
+
data: {
|
|
71
|
+
referenceExportedVarName,
|
|
72
|
+
referenceFile,
|
|
73
|
+
},
|
|
74
|
+
lang: appState.lang,
|
|
75
|
+
log
|
|
76
|
+
})
|
|
77
|
+
)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const referenceKeys = new Set(Object.keys(referenceData))
|
|
81
|
+
log.V(`Reference file contains ${referenceKeys.size} keys`)
|
|
82
|
+
|
|
83
|
+
// Get target languages
|
|
84
|
+
const targetLanguages = options.targetLanguages || config.targetLanguages
|
|
85
|
+
if (!targetLanguages || !targetLanguages.length) {
|
|
86
|
+
throw new Error(
|
|
87
|
+
localize({ token: 'error-no-target-languages', lang: appState.lang, log })
|
|
88
|
+
)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const normalizeOutputFilenames = options.normalizeOutputFilenames || config.normalizeOutputFilenames
|
|
92
|
+
|
|
93
|
+
let totalKeysRemoved = 0
|
|
94
|
+
let filesModified = 0
|
|
95
|
+
|
|
96
|
+
// Process each target language file
|
|
97
|
+
for (const targetLang of targetLanguages) {
|
|
98
|
+
const outputFilePath = normalizeOutputPath({
|
|
99
|
+
dir: outputDir,
|
|
100
|
+
filename: `${targetLang}.json`,
|
|
101
|
+
normalize: normalizeOutputFilenames
|
|
102
|
+
})
|
|
103
|
+
log.D(`Processing ${outputFilePath}...`)
|
|
104
|
+
|
|
105
|
+
// Read existing output data
|
|
106
|
+
const outputData = await readJsonFile(outputFilePath)
|
|
107
|
+
if (!outputData) {
|
|
108
|
+
log.V(`File ${outputFilePath} does not exist, skipping...`)
|
|
109
|
+
continue
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const keysToRemove = []
|
|
113
|
+
for (const key of Object.keys(outputData)) {
|
|
114
|
+
if (!referenceKeys.has(key)) {
|
|
115
|
+
keysToRemove.push(key)
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (keysToRemove.length > 0) {
|
|
120
|
+
log.I(`Found ${keysToRemove.length} obsolete key(s) in ${targetLang}.json:`)
|
|
121
|
+
keysToRemove.forEach(key => {
|
|
122
|
+
log.I(` - ${key}`)
|
|
123
|
+
delete outputData[key]
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
// Write the pruned file
|
|
127
|
+
if (!options.dryRun) {
|
|
128
|
+
writeJsonFile(outputFilePath, outputData, log)
|
|
129
|
+
log.V(`Wrote ${outputFilePath}`)
|
|
130
|
+
filesModified++
|
|
131
|
+
}
|
|
132
|
+
totalKeysRemoved += keysToRemove.length
|
|
133
|
+
} else {
|
|
134
|
+
log.V(`No obsolete keys found in ${targetLang}.json`)
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (options.dryRun) {
|
|
139
|
+
log.I(`\nDry run complete. Would have removed ${totalKeysRemoved} key(s) from ${filesModified} file(s).`)
|
|
140
|
+
} else if (totalKeysRemoved > 0) {
|
|
141
|
+
log.I(`\nRemoved ${totalKeysRemoved} obsolete key(s) from ${filesModified} file(s).`)
|
|
142
|
+
} else {
|
|
143
|
+
log.I(`\nNo obsolete keys found. All target files are up to date.`)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
} catch (error) {
|
|
147
|
+
log.E(error)
|
|
148
|
+
exitCode = 2
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
await shutdown(appState, false)
|
|
152
|
+
|
|
153
|
+
if (exitCode > 0) {
|
|
154
|
+
process.exit(exitCode)
|
|
155
|
+
}
|
|
156
|
+
}
|
|
@@ -366,6 +366,11 @@ export async function runTranslation({ appState, options, log }) {
|
|
|
366
366
|
log.T(taskInfo)
|
|
367
367
|
const progress = 100 * Math.floor(100 * taskInfoIdx / totalTasks) / 100
|
|
368
368
|
|
|
369
|
+
// Broadcast progress for CI
|
|
370
|
+
if (process.env.CI) {
|
|
371
|
+
console.log(`::notice::${progress}% - ${taskInfo.targetLang}/${taskInfo.key}`)
|
|
372
|
+
}
|
|
373
|
+
|
|
369
374
|
await new Listr([
|
|
370
375
|
{
|
|
371
376
|
title: localizeFormatted({
|
package/src/main.mjs
CHANGED
|
@@ -15,6 +15,7 @@ import { keyList, languageList } from './lib/options.js'
|
|
|
15
15
|
import { runTranslation } from './commands/translate.js'
|
|
16
16
|
import { registerSignalHandlers } from './shutdown.js'
|
|
17
17
|
import { runListModels } from './commands/list-models.js'
|
|
18
|
+
import { runPrune } from './commands/prune.js'
|
|
18
19
|
|
|
19
20
|
const __dirname = path.dirname(
|
|
20
21
|
fileURLToPath(import.meta.url)
|
|
@@ -99,6 +100,10 @@ export async function run() {
|
|
|
99
100
|
case 'list-models':
|
|
100
101
|
await runListModels({ appState, options, log })
|
|
101
102
|
break
|
|
103
|
+
|
|
104
|
+
case 'prune':
|
|
105
|
+
await runPrune({ appState, options, log })
|
|
106
|
+
break
|
|
102
107
|
}
|
|
103
108
|
}
|
|
104
109
|
|
|
@@ -145,6 +150,23 @@ export async function run() {
|
|
|
145
150
|
.action(runCommand)
|
|
146
151
|
})
|
|
147
152
|
|
|
153
|
+
program
|
|
154
|
+
.command('prune')
|
|
155
|
+
.description('Remove keys from target files that no longer exist in the reference file')
|
|
156
|
+
.option('-c, --config-file <path>', `Path to config file; defaults to "${DEFAULT_CONFIG_FILENAME}" in the current working directory if not specified`)
|
|
157
|
+
.option('-r, --reference-file <path>', `Path to reference file of source strings. This file can be in .js, .mjs, .json, or .jsonc formats; overrides any 'referenceFile' config setting`)
|
|
158
|
+
.option('-o, --output-dir <path>', `Output directory for localized files; overrides any 'outputDir' config setting`)
|
|
159
|
+
.option('-tl, --target-languages <list>', `Comma-separated list of language codes; overrides any 'targetLanguages' config setting`, value => languageList(value, log))
|
|
160
|
+
.option('-R, --reference-exported-var-name <var name>', `For .js or .mjs reference files only, this will be the exported variable, e.g. for 'export default = {...}' you'd use 'default' here, or 'data' for 'export const data = { ... }'. For .json or .jsonc reference files, this value is ignored.`, 'default')
|
|
161
|
+
.option('-n, --normalize-output-filenames', `Normalizes output filenames (to all lower-case); overrides any 'normalizeOutputFilenames' in config setting`, false)
|
|
162
|
+
.option('--dry-run', 'Show what would be removed without actually modifying files', false)
|
|
163
|
+
.option('-N, --no-logo', `Suppress logo printout`, true) // NB: maps to options.logo, not options.noLogo
|
|
164
|
+
.option('-v, --verbose', `Enables verbose spew`, false)
|
|
165
|
+
.option('-d, --debug', `Enables debug spew`, false)
|
|
166
|
+
.option('-t, --trace', `Enables trace spew`, false)
|
|
167
|
+
.option('--dev', `Enable dev mode, which prints stack traces with errors`, false)
|
|
168
|
+
.action(runCommand)
|
|
169
|
+
|
|
148
170
|
program.parse(process.argv)
|
|
149
171
|
} catch (error) {
|
|
150
172
|
log.E(error)
|
package/test/common.mjs
CHANGED
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
import { execa } from 'execa'
|
|
2
|
+
import { expect } from 'chai'
|
|
3
|
+
import fs from 'fs'
|
|
4
|
+
import path from 'path'
|
|
5
|
+
import { fileURLToPath } from 'url'
|
|
6
|
+
import { dirname } from 'path'
|
|
7
|
+
import crypto from 'crypto'
|
|
8
|
+
import { SRC_DATA_DIR, cleanupFile, cleanupCacheFile } from './common.mjs'
|
|
9
|
+
|
|
10
|
+
const __filename = fileURLToPath(import.meta.url)
|
|
11
|
+
const __dirname = dirname(__filename)
|
|
12
|
+
|
|
13
|
+
describe('prune command', () => {
|
|
14
|
+
it('should remove obsolete keys from target files', async function() {
|
|
15
|
+
this.timeout(10000)
|
|
16
|
+
|
|
17
|
+
// Generate a random ID for the reference file
|
|
18
|
+
const randomId = crypto.randomBytes(4).toString('hex')
|
|
19
|
+
const refFileName = `ref-${randomId}.json`
|
|
20
|
+
const refFilePath = path.join(SRC_DATA_DIR, refFileName)
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
// Create a reference file with only 2 keys
|
|
24
|
+
const referenceData = {
|
|
25
|
+
'msg-test': 'Nothing to do',
|
|
26
|
+
'error-finished': 'Finished with %%errorsEncountered%% error%%s%%'
|
|
27
|
+
}
|
|
28
|
+
fs.writeFileSync(refFilePath, JSON.stringify(referenceData, null, 2), 'utf8')
|
|
29
|
+
|
|
30
|
+
// Create a target file with 3 keys (one obsolete)
|
|
31
|
+
const targetFilePath = path.join(SRC_DATA_DIR, 'fr-FR.json')
|
|
32
|
+
const targetData = {
|
|
33
|
+
'msg-test': 'Rien à faire',
|
|
34
|
+
'error-finished': 'Terminé avec %%errorsEncountered%% erreur%%s%%',
|
|
35
|
+
'obsolete-key': 'This should be removed'
|
|
36
|
+
}
|
|
37
|
+
fs.writeFileSync(targetFilePath, JSON.stringify(targetData, null, 2), 'utf8')
|
|
38
|
+
|
|
39
|
+
// Run the prune command
|
|
40
|
+
const result = await execa('node', [
|
|
41
|
+
path.resolve(__dirname, '../alt.mjs'),
|
|
42
|
+
'prune',
|
|
43
|
+
'-r',
|
|
44
|
+
refFilePath,
|
|
45
|
+
'-tl',
|
|
46
|
+
'fr-FR',
|
|
47
|
+
'-d'
|
|
48
|
+
], {
|
|
49
|
+
cwd: 'test'
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
// Check command executed successfully
|
|
53
|
+
expect(result.exitCode).to.equal(0)
|
|
54
|
+
|
|
55
|
+
// Verify the target file was pruned
|
|
56
|
+
const prunedContent = JSON.parse(fs.readFileSync(targetFilePath, 'utf8'))
|
|
57
|
+
expect(prunedContent).to.have.property('msg-test')
|
|
58
|
+
expect(prunedContent).to.have.property('error-finished')
|
|
59
|
+
expect(prunedContent).to.not.have.property('obsolete-key')
|
|
60
|
+
|
|
61
|
+
// Clean up
|
|
62
|
+
cleanupFile(targetFilePath)
|
|
63
|
+
} finally {
|
|
64
|
+
cleanupFile(refFilePath)
|
|
65
|
+
}
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
it('should handle multiple target languages', async function() {
|
|
69
|
+
this.timeout(10000)
|
|
70
|
+
|
|
71
|
+
const randomId = crypto.randomBytes(4).toString('hex')
|
|
72
|
+
const refFileName = `ref-${randomId}.json`
|
|
73
|
+
const refFilePath = path.join(SRC_DATA_DIR, refFileName)
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
// Create reference file
|
|
77
|
+
const referenceData = {
|
|
78
|
+
'msg-test': 'Nothing to do'
|
|
79
|
+
}
|
|
80
|
+
fs.writeFileSync(refFilePath, JSON.stringify(referenceData, null, 2), 'utf8')
|
|
81
|
+
|
|
82
|
+
// Create multiple target files with obsolete keys
|
|
83
|
+
const frFilePath = path.join(SRC_DATA_DIR, 'fr-FR.json')
|
|
84
|
+
const esFilePath = path.join(SRC_DATA_DIR, 'es-ES.json')
|
|
85
|
+
|
|
86
|
+
const frData = {
|
|
87
|
+
'msg-test': 'Rien à faire',
|
|
88
|
+
'obsolete-fr': 'Obsolete French key'
|
|
89
|
+
}
|
|
90
|
+
const esData = {
|
|
91
|
+
'msg-test': 'Nada que hacer',
|
|
92
|
+
'obsolete-es': 'Obsolete Spanish key'
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
fs.writeFileSync(frFilePath, JSON.stringify(frData, null, 2), 'utf8')
|
|
96
|
+
fs.writeFileSync(esFilePath, JSON.stringify(esData, null, 2), 'utf8')
|
|
97
|
+
|
|
98
|
+
// Run the prune command
|
|
99
|
+
const result = await execa('node', [
|
|
100
|
+
path.resolve(__dirname, '../alt.mjs'),
|
|
101
|
+
'prune',
|
|
102
|
+
'-r',
|
|
103
|
+
refFilePath,
|
|
104
|
+
'-tl',
|
|
105
|
+
'fr-FR,es-ES'
|
|
106
|
+
], {
|
|
107
|
+
cwd: 'test'
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
// Check command executed successfully
|
|
111
|
+
expect(result.exitCode).to.equal(0)
|
|
112
|
+
|
|
113
|
+
// Verify both files were pruned
|
|
114
|
+
const prunedFrContent = JSON.parse(fs.readFileSync(frFilePath, 'utf8'))
|
|
115
|
+
const prunedEsContent = JSON.parse(fs.readFileSync(esFilePath, 'utf8'))
|
|
116
|
+
|
|
117
|
+
expect(prunedFrContent).to.have.property('msg-test')
|
|
118
|
+
expect(prunedFrContent).to.not.have.property('obsolete-fr')
|
|
119
|
+
|
|
120
|
+
expect(prunedEsContent).to.have.property('msg-test')
|
|
121
|
+
expect(prunedEsContent).to.not.have.property('obsolete-es')
|
|
122
|
+
|
|
123
|
+
// Clean up
|
|
124
|
+
cleanupFile(frFilePath)
|
|
125
|
+
cleanupFile(esFilePath)
|
|
126
|
+
} finally {
|
|
127
|
+
cleanupFile(refFilePath)
|
|
128
|
+
}
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
it('should support dry-run mode without modifying files', async function() {
|
|
132
|
+
this.timeout(10000)
|
|
133
|
+
|
|
134
|
+
const randomId = crypto.randomBytes(4).toString('hex')
|
|
135
|
+
const refFileName = `ref-${randomId}.json`
|
|
136
|
+
const refFilePath = path.join(SRC_DATA_DIR, refFileName)
|
|
137
|
+
|
|
138
|
+
try {
|
|
139
|
+
// Create reference file with 1 key
|
|
140
|
+
const referenceData = {
|
|
141
|
+
'msg-test': 'Nothing to do'
|
|
142
|
+
}
|
|
143
|
+
fs.writeFileSync(refFilePath, JSON.stringify(referenceData, null, 2), 'utf8')
|
|
144
|
+
|
|
145
|
+
// Create target file with 2 keys (one obsolete)
|
|
146
|
+
const targetFilePath = path.join(SRC_DATA_DIR, 'fr-FR.json')
|
|
147
|
+
const targetData = {
|
|
148
|
+
'msg-test': 'Rien à faire',
|
|
149
|
+
'obsolete-key': 'This should NOT be removed in dry-run'
|
|
150
|
+
}
|
|
151
|
+
fs.writeFileSync(targetFilePath, JSON.stringify(targetData, null, 2), 'utf8')
|
|
152
|
+
|
|
153
|
+
// Run the prune command with --dry-run
|
|
154
|
+
const result = await execa('node', [
|
|
155
|
+
path.resolve(__dirname, '../alt.mjs'),
|
|
156
|
+
'prune',
|
|
157
|
+
'-r',
|
|
158
|
+
refFilePath,
|
|
159
|
+
'-tl',
|
|
160
|
+
'fr-FR',
|
|
161
|
+
'--dry-run'
|
|
162
|
+
], {
|
|
163
|
+
cwd: 'test'
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
// Check command executed successfully
|
|
167
|
+
expect(result.exitCode).to.equal(0)
|
|
168
|
+
|
|
169
|
+
// Verify the target file was NOT modified
|
|
170
|
+
const unchangedContent = JSON.parse(fs.readFileSync(targetFilePath, 'utf8'))
|
|
171
|
+
expect(unchangedContent).to.have.property('msg-test')
|
|
172
|
+
expect(unchangedContent).to.have.property('obsolete-key', 'This should NOT be removed in dry-run')
|
|
173
|
+
|
|
174
|
+
// Clean up
|
|
175
|
+
cleanupFile(targetFilePath)
|
|
176
|
+
} finally {
|
|
177
|
+
cleanupFile(refFilePath)
|
|
178
|
+
}
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
it('should handle non-existent target files gracefully', async function() {
|
|
182
|
+
this.timeout(10000)
|
|
183
|
+
|
|
184
|
+
const randomId = crypto.randomBytes(4).toString('hex')
|
|
185
|
+
const refFileName = `ref-${randomId}.json`
|
|
186
|
+
const refFilePath = path.join(SRC_DATA_DIR, refFileName)
|
|
187
|
+
|
|
188
|
+
try {
|
|
189
|
+
// Create reference file
|
|
190
|
+
const referenceData = {
|
|
191
|
+
'msg-test': 'Nothing to do'
|
|
192
|
+
}
|
|
193
|
+
fs.writeFileSync(refFilePath, JSON.stringify(referenceData, null, 2), 'utf8')
|
|
194
|
+
|
|
195
|
+
// Don't create any target files
|
|
196
|
+
|
|
197
|
+
// Run the prune command
|
|
198
|
+
const result = await execa('node', [
|
|
199
|
+
path.resolve(__dirname, '../alt.mjs'),
|
|
200
|
+
'prune',
|
|
201
|
+
'-r',
|
|
202
|
+
refFilePath,
|
|
203
|
+
'-tl',
|
|
204
|
+
'fr-FR'
|
|
205
|
+
], {
|
|
206
|
+
cwd: 'test'
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
// Check command executed successfully
|
|
210
|
+
expect(result.exitCode).to.equal(0)
|
|
211
|
+
} finally {
|
|
212
|
+
cleanupFile(refFilePath)
|
|
213
|
+
}
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
it('should keep all keys when no obsolete keys exist', async function() {
|
|
217
|
+
this.timeout(10000)
|
|
218
|
+
|
|
219
|
+
const randomId = crypto.randomBytes(4).toString('hex')
|
|
220
|
+
const refFileName = `ref-${randomId}.json`
|
|
221
|
+
const refFilePath = path.join(SRC_DATA_DIR, refFileName)
|
|
222
|
+
|
|
223
|
+
try {
|
|
224
|
+
// Create reference file with 2 keys
|
|
225
|
+
const referenceData = {
|
|
226
|
+
'msg-test': 'Nothing to do',
|
|
227
|
+
'error-finished': 'Finished with %%errorsEncountered%% error%%s%%'
|
|
228
|
+
}
|
|
229
|
+
fs.writeFileSync(refFilePath, JSON.stringify(referenceData, null, 2), 'utf8')
|
|
230
|
+
|
|
231
|
+
// Create target file with same keys (no obsolete keys)
|
|
232
|
+
const targetFilePath = path.join(SRC_DATA_DIR, 'fr-FR.json')
|
|
233
|
+
const targetData = {
|
|
234
|
+
'msg-test': 'Rien à faire',
|
|
235
|
+
'error-finished': 'Terminé avec %%errorsEncountered%% erreur%%s%%'
|
|
236
|
+
}
|
|
237
|
+
fs.writeFileSync(targetFilePath, JSON.stringify(targetData, null, 2), 'utf8')
|
|
238
|
+
|
|
239
|
+
// Run the prune command
|
|
240
|
+
const result = await execa('node', [
|
|
241
|
+
path.resolve(__dirname, '../alt.mjs'),
|
|
242
|
+
'prune',
|
|
243
|
+
'-r',
|
|
244
|
+
refFilePath,
|
|
245
|
+
'-tl',
|
|
246
|
+
'fr-FR'
|
|
247
|
+
], {
|
|
248
|
+
cwd: 'test'
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
// Check command executed successfully
|
|
252
|
+
expect(result.exitCode).to.equal(0)
|
|
253
|
+
|
|
254
|
+
// Verify all keys are still present
|
|
255
|
+
const unchangedContent = JSON.parse(fs.readFileSync(targetFilePath, 'utf8'))
|
|
256
|
+
expect(unchangedContent).to.have.property('msg-test')
|
|
257
|
+
expect(unchangedContent).to.have.property('error-finished')
|
|
258
|
+
expect(Object.keys(unchangedContent).length).to.equal(2)
|
|
259
|
+
|
|
260
|
+
// Clean up
|
|
261
|
+
cleanupFile(targetFilePath)
|
|
262
|
+
} finally {
|
|
263
|
+
cleanupFile(refFilePath)
|
|
264
|
+
}
|
|
265
|
+
})
|
|
266
|
+
|
|
267
|
+
it('should work with .js reference files', async function() {
|
|
268
|
+
this.timeout(10000)
|
|
269
|
+
|
|
270
|
+
const randomId = crypto.randomBytes(4).toString('hex')
|
|
271
|
+
const refFileName = `ref-${randomId}.js`
|
|
272
|
+
const refFilePath = path.join(SRC_DATA_DIR, refFileName)
|
|
273
|
+
|
|
274
|
+
try {
|
|
275
|
+
// Create a .js reference file
|
|
276
|
+
const refContent = `export default {
|
|
277
|
+
'msg-test': 'Nothing to do',
|
|
278
|
+
'error-finished': 'Finished with errors'
|
|
279
|
+
}`
|
|
280
|
+
fs.writeFileSync(refFilePath, refContent, 'utf8')
|
|
281
|
+
|
|
282
|
+
// Create target file with an obsolete key
|
|
283
|
+
const targetFilePath = path.join(SRC_DATA_DIR, 'fr-FR.json')
|
|
284
|
+
const targetData = {
|
|
285
|
+
'msg-test': 'Rien à faire',
|
|
286
|
+
'error-finished': 'Terminé avec erreurs',
|
|
287
|
+
'obsolete-key': 'Should be removed'
|
|
288
|
+
}
|
|
289
|
+
fs.writeFileSync(targetFilePath, JSON.stringify(targetData, null, 2), 'utf8')
|
|
290
|
+
|
|
291
|
+
// Run the prune command
|
|
292
|
+
const result = await execa('node', [
|
|
293
|
+
path.resolve(__dirname, '../alt.mjs'),
|
|
294
|
+
'prune',
|
|
295
|
+
'-r',
|
|
296
|
+
refFilePath,
|
|
297
|
+
'-tl',
|
|
298
|
+
'fr-FR',
|
|
299
|
+
'-R',
|
|
300
|
+
'default'
|
|
301
|
+
], {
|
|
302
|
+
cwd: 'test'
|
|
303
|
+
})
|
|
304
|
+
|
|
305
|
+
// Check command executed successfully
|
|
306
|
+
expect(result.exitCode).to.equal(0)
|
|
307
|
+
|
|
308
|
+
// Verify the obsolete key was removed
|
|
309
|
+
const prunedContent = JSON.parse(fs.readFileSync(targetFilePath, 'utf8'))
|
|
310
|
+
expect(prunedContent).to.have.property('msg-test')
|
|
311
|
+
expect(prunedContent).to.have.property('error-finished')
|
|
312
|
+
expect(prunedContent).to.not.have.property('obsolete-key')
|
|
313
|
+
|
|
314
|
+
// Clean up
|
|
315
|
+
cleanupFile(targetFilePath)
|
|
316
|
+
} finally {
|
|
317
|
+
cleanupFile(refFilePath)
|
|
318
|
+
}
|
|
319
|
+
})
|
|
320
|
+
})
|