i18ntk 3.3.0 → 4.1.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/CHANGELOG.md +84 -16
- package/README.md +160 -15
- package/SECURITY.md +16 -8
- package/main/i18ntk-backup.js +370 -73
- package/main/i18ntk-scanner.js +190 -49
- package/main/i18ntk-sizing.js +241 -79
- package/main/i18ntk-usage.js +221 -46
- package/main/i18ntk-validate.js +114 -5
- package/main/manage/commands/FixerCommand.js +23 -21
- package/main/manage/index.js +13 -7
- package/main/manage/services/FileManagementService.js +12 -6
- package/package.json +46 -2
- package/runtime/i18ntk.d.ts +22 -16
- package/runtime/index.d.ts +9 -7
- package/runtime/index.js +246 -50
- package/ui-locales/en.json +1 -1
- package/utils/translate/protection.js +153 -7
- package/utils/watch-locales.js +194 -36
package/utils/watch-locales.js
CHANGED
|
@@ -1,36 +1,194 @@
|
|
|
1
|
-
const fs = require('fs');
|
|
2
|
-
const path = require('path');
|
|
3
|
-
const
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const EventEmitter = require('events');
|
|
4
|
+
const crypto = require('crypto');
|
|
5
|
+
const SecurityUtils = require('./security');
|
|
6
|
+
|
|
7
|
+
const DEFAULT_DEBOUNCE_MS = 300;
|
|
8
|
+
const DEFAULT_MAX_DIRECTORIES = 50;
|
|
9
|
+
|
|
10
|
+
function sha256File(filePath) {
|
|
11
|
+
try {
|
|
12
|
+
const content = SecurityUtils.safeReadFileSync(filePath, path.dirname(filePath));
|
|
13
|
+
if (content === null || content === undefined) return null;
|
|
14
|
+
return crypto.createHash('sha256').update(content).digest('hex');
|
|
15
|
+
} catch (_) {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function watchDirectory(dir, emitter, watchers, options = {}) {
|
|
21
|
+
const {
|
|
22
|
+
debounceMs = DEFAULT_DEBOUNCE_MS,
|
|
23
|
+
hashTracking = true,
|
|
24
|
+
watchState = { count: 0, maxDirectories: DEFAULT_MAX_DIRECTORIES }
|
|
25
|
+
} = options;
|
|
26
|
+
|
|
27
|
+
if (!SecurityUtils.safeExistsSync(dir, path.dirname(dir))) return;
|
|
28
|
+
if (watchState.count >= watchState.maxDirectories) {
|
|
29
|
+
emitter.emit('error', new Error(`Maximum watched directories (${watchState.maxDirectories}) exceeded`));
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const fileHashes = new Map();
|
|
34
|
+
const debounceTimers = new Map();
|
|
35
|
+
|
|
36
|
+
if (hashTracking) {
|
|
37
|
+
try {
|
|
38
|
+
const items = SecurityUtils.safeReaddirSync(dir, path.dirname(dir), { withFileTypes: true });
|
|
39
|
+
if (items) {
|
|
40
|
+
for (const entry of items) {
|
|
41
|
+
if (entry.isFile() && entry.name.endsWith('.json')) {
|
|
42
|
+
const fullPath = path.join(dir, entry.name);
|
|
43
|
+
const h = sha256File(fullPath);
|
|
44
|
+
if (h) fileHashes.set(fullPath, h);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
} catch (_) { /* initial read may fail */ }
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
let watcher;
|
|
52
|
+
try {
|
|
53
|
+
watcher = fs.watch(dir, (event, filename) => {
|
|
54
|
+
if (!filename || !filename.endsWith('.json')) return;
|
|
55
|
+
|
|
56
|
+
const fullPath = path.join(dir, filename);
|
|
57
|
+
const validated = SecurityUtils.validatePath(fullPath, path.dirname(dir));
|
|
58
|
+
if (!validated) return;
|
|
59
|
+
|
|
60
|
+
if (debounceTimers.has(fullPath)) {
|
|
61
|
+
clearTimeout(debounceTimers.get(fullPath));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
debounceTimers.set(fullPath, setTimeout(() => {
|
|
65
|
+
debounceTimers.delete(fullPath);
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
if (event === 'rename') {
|
|
69
|
+
if (SecurityUtils.safeExistsSync(fullPath, path.dirname(fullPath))) {
|
|
70
|
+
if (hashTracking) {
|
|
71
|
+
const h = sha256File(fullPath);
|
|
72
|
+
if (h) fileHashes.set(fullPath, h);
|
|
73
|
+
}
|
|
74
|
+
emitter.emit('add', fullPath);
|
|
75
|
+
} else {
|
|
76
|
+
fileHashes.delete(fullPath);
|
|
77
|
+
emitter.emit('unlink', fullPath);
|
|
78
|
+
}
|
|
79
|
+
} else if (event === 'change') {
|
|
80
|
+
if (hashTracking) {
|
|
81
|
+
const newHash = sha256File(fullPath);
|
|
82
|
+
const oldHash = fileHashes.get(fullPath);
|
|
83
|
+
if (newHash && newHash === oldHash) return;
|
|
84
|
+
if (newHash) fileHashes.set(fullPath, newHash);
|
|
85
|
+
}
|
|
86
|
+
emitter.emit('change', fullPath);
|
|
87
|
+
}
|
|
88
|
+
} catch (err) {
|
|
89
|
+
emitter.emit('error', err);
|
|
90
|
+
}
|
|
91
|
+
}, debounceMs));
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
watcher.on('error', (err) => {
|
|
95
|
+
emitter.emit('error', err);
|
|
96
|
+
});
|
|
97
|
+
} catch (err) {
|
|
98
|
+
emitter.emit('error', err);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
watchers.push({ watcher, path: dir, debounceTimers });
|
|
103
|
+
watchState.count++;
|
|
104
|
+
|
|
105
|
+
try {
|
|
106
|
+
const items = SecurityUtils.safeReaddirSync(dir, path.dirname(dir), { withFileTypes: true });
|
|
107
|
+
if (items) {
|
|
108
|
+
for (const entry of items) {
|
|
109
|
+
if (entry.isDirectory()) {
|
|
110
|
+
watchDirectory(path.join(dir, entry.name), emitter, watchers, options);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
} catch (_) {
|
|
115
|
+
// Cannot read directory contents
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function watchLocales(dirs, onChange, options = {}) {
|
|
120
|
+
const directories = Array.isArray(dirs) ? dirs : [dirs];
|
|
121
|
+
const emitter = new EventEmitter();
|
|
122
|
+
emitter.on('error', () => {});
|
|
123
|
+
const watchers = [];
|
|
124
|
+
|
|
125
|
+
// Backward-compatible onChange callback
|
|
126
|
+
if (typeof onChange === 'function') {
|
|
127
|
+
emitter.on('change', onChange);
|
|
128
|
+
emitter.on('add', onChange);
|
|
129
|
+
emitter.on('unlink', onChange);
|
|
130
|
+
} else if (typeof onChange === 'object' && onChange !== null && typeof onChange.onChange === 'function') {
|
|
131
|
+
emitter.on('change', onChange.onChange);
|
|
132
|
+
emitter.on('add', onChange.onChange);
|
|
133
|
+
emitter.on('unlink', onChange.onChange);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const {
|
|
137
|
+
debounceMs = DEFAULT_DEBOUNCE_MS,
|
|
138
|
+
hashTracking = true,
|
|
139
|
+
maxDirectories = DEFAULT_MAX_DIRECTORIES
|
|
140
|
+
} = (typeof onChange === 'object' && onChange !== null) ? onChange : options;
|
|
141
|
+
|
|
142
|
+
const watchState = { count: 0, maxDirectories };
|
|
143
|
+
for (const d of directories) {
|
|
144
|
+
if (watchState.count >= watchState.maxDirectories) {
|
|
145
|
+
emitter.emit('error', new Error(`Maximum watched directories (${watchState.maxDirectories}) exceeded`));
|
|
146
|
+
break;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const resolved = path.resolve(d);
|
|
150
|
+
const validated = SecurityUtils.validatePath(resolved, process.cwd());
|
|
151
|
+
if (!validated) {
|
|
152
|
+
emitter.emit('error', new Error(`Path validation failed for: ${d}`));
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const projectRoot = path.resolve(process.cwd());
|
|
157
|
+
const rel = path.relative(projectRoot, validated);
|
|
158
|
+
if (rel.startsWith('..')) {
|
|
159
|
+
emitter.emit('error', new Error(`Directory outside project root: ${d}`));
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
watchDirectory(validated, emitter, watchers, { debounceMs, hashTracking, watchState });
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const stop = () => {
|
|
167
|
+
for (const entry of watchers) {
|
|
168
|
+
if (entry.debounceTimers) {
|
|
169
|
+
for (const timer of entry.debounceTimers.values()) {
|
|
170
|
+
clearTimeout(timer);
|
|
171
|
+
}
|
|
172
|
+
entry.debounceTimers.clear();
|
|
173
|
+
}
|
|
174
|
+
try { entry.watcher.close(); } catch (_) { /* ignore */ }
|
|
175
|
+
}
|
|
176
|
+
watchers.length = 0;
|
|
177
|
+
emitter.removeAllListeners();
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
stop.stop = stop;
|
|
181
|
+
stop.emitter = emitter;
|
|
182
|
+
stop.on = emitter.on.bind(emitter);
|
|
183
|
+
stop.once = emitter.once.bind(emitter);
|
|
184
|
+
stop.off = emitter.off ? emitter.off.bind(emitter) : emitter.removeListener.bind(emitter);
|
|
185
|
+
stop.emit = emitter.emit.bind(emitter);
|
|
186
|
+
stop.removeListener = emitter.removeListener.bind(emitter);
|
|
187
|
+
stop.removeAllListeners = emitter.removeAllListeners.bind(emitter);
|
|
188
|
+
stop.getWatchedPaths = () => watchers.map(w => w.path);
|
|
189
|
+
stop.getDebounceMs = () => debounceMs;
|
|
190
|
+
|
|
191
|
+
return stop;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
module.exports = watchLocales;
|