i18ntk 4.3.0 → 4.3.1

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.
@@ -0,0 +1,162 @@
1
+ const path = require('path');
2
+ const SecurityUtils = require('./security');
3
+
4
+ const LANGUAGE_PREFIX_PATTERN = /^\s*\[[A-Za-z]{2,3}(?:[-_][A-Za-z0-9]{2,8})?\]\s+\S/;
5
+
6
+ function isLanguagePrefixPlaceholder(value) {
7
+ return typeof value === 'string' && LANGUAGE_PREFIX_PATTERN.test(value);
8
+ }
9
+
10
+ function collectStringLeaves(value, prefix = '') {
11
+ const leaves = [];
12
+
13
+ if (typeof value === 'string') {
14
+ leaves.push({ key: prefix || '<root>', value });
15
+ return leaves;
16
+ }
17
+
18
+ if (Array.isArray(value)) {
19
+ value.forEach((item, index) => {
20
+ const nextPrefix = `${prefix}[${index}]`;
21
+ leaves.push(...collectStringLeaves(item, nextPrefix));
22
+ });
23
+ return leaves;
24
+ }
25
+
26
+ if (value && typeof value === 'object') {
27
+ for (const [key, child] of Object.entries(value)) {
28
+ const nextPrefix = prefix ? `${prefix}.${key}` : key;
29
+ leaves.push(...collectStringLeaves(child, nextPrefix));
30
+ }
31
+ }
32
+
33
+ return leaves;
34
+ }
35
+
36
+ function collectJsonFiles(dir, rootDir = dir) {
37
+ const baseDir = path.resolve(rootDir);
38
+ const validatedDir = SecurityUtils.validatePath(path.resolve(dir), baseDir);
39
+ if (!validatedDir || !SecurityUtils.safeExistsSync(validatedDir, baseDir)) return [];
40
+
41
+ const stat = SecurityUtils.safeStatSync(validatedDir, baseDir);
42
+ if (!stat.isDirectory()) return [];
43
+
44
+ const results = [];
45
+ for (const entry of SecurityUtils.safeReaddirSync(validatedDir, baseDir, { withFileTypes: true })) {
46
+ if (entry.name.startsWith('.') || entry.name === 'node_modules') continue;
47
+
48
+ const fullPath = path.join(validatedDir, entry.name);
49
+ if (entry.isDirectory()) {
50
+ results.push(...collectJsonFiles(fullPath, baseDir));
51
+ } else if (entry.isFile() && entry.name.endsWith('.json')) {
52
+ results.push({
53
+ fullPath,
54
+ displayPath: path.relative(baseDir, fullPath) || entry.name
55
+ });
56
+ }
57
+ }
58
+
59
+ return results;
60
+ }
61
+
62
+ function getEnglishLocaleFiles(sourceDir, sourceLanguage = 'en') {
63
+ const requestedRoot = path.resolve(sourceDir || './locales');
64
+ const localeRoot = SecurityUtils.validatePath(requestedRoot, process.cwd());
65
+ if (!localeRoot) return [];
66
+
67
+ const files = [];
68
+ const seen = new Set();
69
+
70
+ const addFile = (file) => {
71
+ const resolved = path.resolve(file.fullPath);
72
+ if (seen.has(resolved)) return;
73
+ seen.add(resolved);
74
+ files.push(file);
75
+ };
76
+
77
+ const languageDir = path.join(localeRoot, sourceLanguage);
78
+ for (const file of collectJsonFiles(languageDir, languageDir)) {
79
+ addFile(file);
80
+ }
81
+
82
+ const monolithFile = path.join(localeRoot, `${sourceLanguage}.json`);
83
+ const monolithStat = SecurityUtils.safeStatSync(monolithFile, localeRoot);
84
+ if (monolithStat && monolithStat.isFile()) {
85
+ addFile({
86
+ fullPath: monolithFile,
87
+ displayPath: path.basename(monolithFile)
88
+ });
89
+ }
90
+
91
+ if (files.length === 0 && path.basename(localeRoot).toLowerCase() === sourceLanguage.toLowerCase()) {
92
+ for (const file of collectJsonFiles(localeRoot, localeRoot)) {
93
+ addFile(file);
94
+ }
95
+ }
96
+
97
+ return files.sort((a, b) => a.displayPath.localeCompare(b.displayPath));
98
+ }
99
+
100
+ function scanEnglishPlaceholders(options = {}) {
101
+ const sourceDir = options.sourceDir || './locales';
102
+ const sourceLanguage = options.sourceLanguage || 'en';
103
+ const localeRoot = SecurityUtils.validatePath(path.resolve(sourceDir), process.cwd()) || process.cwd();
104
+ const files = getEnglishLocaleFiles(sourceDir, sourceLanguage);
105
+ const placeholders = [];
106
+ const errors = [];
107
+ let keyCount = 0;
108
+
109
+ for (const file of files) {
110
+ try {
111
+ const content = SecurityUtils.safeReadFileSync(file.fullPath, localeRoot, 'utf8');
112
+ if (!content) {
113
+ throw new Error('Unable to read locale file');
114
+ }
115
+
116
+ const parseFailed = { __i18ntkParseFailed: true };
117
+ const parsed = SecurityUtils.safeParseJSON(content, parseFailed);
118
+ if (parsed === parseFailed) {
119
+ throw new Error('Invalid JSON content');
120
+ }
121
+
122
+ const leaves = collectStringLeaves(parsed);
123
+ keyCount += leaves.length;
124
+
125
+ for (const leaf of leaves) {
126
+ if (isLanguagePrefixPlaceholder(leaf.value)) {
127
+ placeholders.push({
128
+ file: file.displayPath,
129
+ path: file.fullPath,
130
+ key: leaf.key,
131
+ value: leaf.value
132
+ });
133
+ }
134
+ }
135
+ } catch (error) {
136
+ errors.push({
137
+ file: file.displayPath,
138
+ path: file.fullPath,
139
+ error: error.message
140
+ });
141
+ }
142
+ }
143
+
144
+ return {
145
+ success: placeholders.length === 0 && errors.length === 0,
146
+ sourceDir: path.resolve(sourceDir),
147
+ sourceLanguage,
148
+ fileCount: files.length,
149
+ keyCount,
150
+ placeholderCount: placeholders.length,
151
+ placeholders,
152
+ errors
153
+ };
154
+ }
155
+
156
+ module.exports = {
157
+ LANGUAGE_PREFIX_PATTERN,
158
+ collectStringLeaves,
159
+ getEnglishLocaleFiles,
160
+ isLanguagePrefixPlaceholder,
161
+ scanEnglishPlaceholders
162
+ };
@@ -0,0 +1,179 @@
1
+ /**
2
+ * Minimal zero-dependency subset of commander used by i18ntk language CLIs.
3
+ */
4
+
5
+ function toCamelCase(input) {
6
+ return String(input || '').replace(/-([a-z])/g, (_, char) => char.toUpperCase());
7
+ }
8
+
9
+ function parseOptionDefinition(flags, defaultValue) {
10
+ const longMatch = flags.match(/--([a-zA-Z0-9-]+)/);
11
+ const shortMatch = flags.match(/(^|\s)-([a-zA-Z])(\s|,|$)/);
12
+ const hasValue = /<[^>]+>/.test(flags);
13
+ const longName = longMatch ? longMatch[1] : null;
14
+ const shortName = shortMatch ? shortMatch[2] : null;
15
+ const name = toCamelCase(longName || shortName || '');
16
+
17
+ return {
18
+ flags,
19
+ hasValue,
20
+ defaultValue,
21
+ longFlag: longName ? `--${longName}` : null,
22
+ shortFlag: shortName ? `-${shortName}` : null,
23
+ name
24
+ };
25
+ }
26
+
27
+ function parseOptions(args, optionDefs) {
28
+ const options = {};
29
+
30
+ for (const def of optionDefs) {
31
+ if (def.defaultValue !== undefined) {
32
+ options[def.name] = def.defaultValue;
33
+ } else if (!def.hasValue) {
34
+ options[def.name] = false;
35
+ }
36
+ }
37
+
38
+ for (let i = 0; i < args.length; i++) {
39
+ const arg = args[i];
40
+ if (!arg || !arg.startsWith('-')) {
41
+ continue;
42
+ }
43
+
44
+ let matchedDef = null;
45
+ let inlineValue;
46
+
47
+ if (arg.startsWith('--')) {
48
+ const [flag, value] = arg.split('=', 2);
49
+ matchedDef = optionDefs.find(def => def.longFlag === flag);
50
+ inlineValue = value;
51
+ } else {
52
+ matchedDef = optionDefs.find(def => def.shortFlag === arg);
53
+ }
54
+
55
+ if (!matchedDef) {
56
+ continue;
57
+ }
58
+
59
+ if (!matchedDef.hasValue) {
60
+ options[matchedDef.name] = true;
61
+ continue;
62
+ }
63
+
64
+ let value = inlineValue;
65
+ if (value === undefined) {
66
+ const next = args[i + 1];
67
+ if (next !== undefined && !String(next).startsWith('-')) {
68
+ value = next;
69
+ i++;
70
+ }
71
+ }
72
+
73
+ options[matchedDef.name] = value !== undefined ? value : '';
74
+ }
75
+
76
+ return options;
77
+ }
78
+
79
+ class MiniCommand {
80
+ constructor(name) {
81
+ this._name = name;
82
+ this._description = '';
83
+ this._options = [];
84
+ this._action = null;
85
+ }
86
+
87
+ description(text) {
88
+ this._description = text;
89
+ return this;
90
+ }
91
+
92
+ option(flags, _description, defaultValue) {
93
+ this._options.push(parseOptionDefinition(flags, defaultValue));
94
+ return this;
95
+ }
96
+
97
+ action(handler) {
98
+ this._action = handler;
99
+ return this;
100
+ }
101
+
102
+ execute(args) {
103
+ const parsed = parseOptions(args, this._options);
104
+ if (typeof this._action === 'function') {
105
+ const result = this._action(parsed);
106
+ if (result && typeof result.then === 'function') {
107
+ result.catch(error => {
108
+ console.error(error && error.message ? error.message : String(error));
109
+ process.exit(1);
110
+ });
111
+ }
112
+ }
113
+ }
114
+ }
115
+
116
+ class MiniProgram {
117
+ constructor() {
118
+ this._name = '';
119
+ this._description = '';
120
+ this._version = '';
121
+ this._options = [];
122
+ this._commands = [];
123
+ this._opts = {};
124
+ }
125
+
126
+ name(value) {
127
+ this._name = value;
128
+ return this;
129
+ }
130
+
131
+ description(value) {
132
+ this._description = value;
133
+ return this;
134
+ }
135
+
136
+ version(value) {
137
+ this._version = value;
138
+ return this;
139
+ }
140
+
141
+ option(flags, _description, defaultValue) {
142
+ this._options.push(parseOptionDefinition(flags, defaultValue));
143
+ return this;
144
+ }
145
+
146
+ command(name) {
147
+ const command = new MiniCommand(name);
148
+ this._commands.push(command);
149
+ return command;
150
+ }
151
+
152
+ opts() {
153
+ return { ...this._opts };
154
+ }
155
+
156
+ parse(argv = process.argv) {
157
+ const args = argv.slice(2);
158
+
159
+ if (this._commands.length > 0 && args.length > 0 && !args[0].startsWith('-')) {
160
+ const command = this._commands.find(item => item._name === args[0]);
161
+ if (command) {
162
+ command.execute(args.slice(1));
163
+ return this;
164
+ }
165
+ }
166
+
167
+ this._opts = parseOptions(args, this._options);
168
+ return this;
169
+ }
170
+ }
171
+
172
+ function createProgram() {
173
+ return new MiniProgram();
174
+ }
175
+
176
+ module.exports = {
177
+ createProgram,
178
+ program: createProgram()
179
+ };