millas 0.2.27 → 0.2.29
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/bin/millas.js +12 -2
- package/package.json +2 -1
- package/src/cli.js +117 -20
- package/src/commands/call.js +1 -1
- package/src/commands/createsuperuser.js +137 -182
- package/src/commands/key.js +61 -83
- package/src/commands/lang.js +423 -515
- package/src/commands/make.js +88 -62
- package/src/commands/migrate.js +200 -279
- package/src/commands/new.js +55 -50
- package/src/commands/route.js +78 -80
- package/src/commands/schedule.js +52 -150
- package/src/commands/serve.js +158 -191
- package/src/console/AppCommand.js +106 -0
- package/src/console/BaseCommand.js +726 -0
- package/src/console/CommandContext.js +66 -0
- package/src/console/CommandRegistry.js +88 -0
- package/src/console/Style.js +123 -0
- package/src/console/index.js +12 -3
- package/src/container/AppInitializer.js +10 -0
- package/src/container/Application.js +2 -0
- package/src/facades/DB.js +195 -0
- package/src/index.js +2 -1
- package/src/scaffold/maker.js +102 -42
- package/src/schematics/Collection.js +28 -0
- package/src/schematics/SchematicEngine.js +122 -0
- package/src/schematics/Template.js +99 -0
- package/src/schematics/index.js +7 -0
- package/src/templates/command/default.template.js +14 -0
- package/src/templates/command/schema.json +19 -0
- package/src/templates/controller/default.template.js +10 -0
- package/src/templates/controller/resource.template.js +59 -0
- package/src/templates/controller/schema.json +30 -0
- package/src/templates/job/default.template.js +11 -0
- package/src/templates/job/schema.json +19 -0
- package/src/templates/middleware/default.template.js +11 -0
- package/src/templates/middleware/schema.json +19 -0
- package/src/templates/migration/default.template.js +14 -0
- package/src/templates/migration/schema.json +19 -0
- package/src/templates/model/default.template.js +14 -0
- package/src/templates/model/migration.template.js +17 -0
- package/src/templates/model/schema.json +30 -0
- package/src/templates/service/default.template.js +12 -0
- package/src/templates/service/schema.json +19 -0
- package/src/templates/shape/default.template.js +11 -0
- package/src/templates/shape/schema.json +19 -0
- package/src/validation/BaseValidator.js +3 -0
- package/src/validation/types.js +3 -3
package/src/commands/lang.js
CHANGED
|
@@ -1,589 +1,497 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const
|
|
4
|
-
const path
|
|
5
|
-
const fs
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
if (options.list) {
|
|
42
|
-
if (!fs.existsSync(langPath)) {
|
|
43
|
-
console.log(chalk.yellow('\n lang/ not found — nothing published yet.\n'));
|
|
44
|
-
return;
|
|
45
|
-
}
|
|
46
|
-
const locales = readLocales(langPath);
|
|
47
|
-
if (!locales.length) {
|
|
48
|
-
console.log(chalk.yellow('\n No locale files in lang/ yet.\n'));
|
|
49
|
-
} else {
|
|
50
|
-
console.log(chalk.cyan('\n Available locales:\n'));
|
|
51
|
-
for (const loc of locales) {
|
|
52
|
-
const nsFiles = readNamespaces(langPath, loc);
|
|
53
|
-
const nsList = nsFiles.length ? chalk.gray(` (${nsFiles.join(', ')})`) : '';
|
|
54
|
-
console.log(` ${chalk.white(loc)}${nsList}`);
|
|
55
|
-
}
|
|
56
|
-
console.log('');
|
|
57
|
-
}
|
|
58
|
-
return;
|
|
59
|
-
}
|
|
3
|
+
const BaseCommand = require('../console/BaseCommand');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const {string} = require("../core/validation");
|
|
7
|
+
|
|
8
|
+
const DEFAULT_NS = 'messages';
|
|
9
|
+
|
|
10
|
+
class LangCommand extends BaseCommand {
|
|
11
|
+
static description = 'Manage application translations';
|
|
12
|
+
|
|
13
|
+
async onInit(register) {
|
|
14
|
+
register
|
|
15
|
+
.command(this.publish)
|
|
16
|
+
.arg('locale', 'Target locale (e.g., sw, fr)')
|
|
17
|
+
.arg('namespace',v=>v.string(), 'Specific namespace to publish')
|
|
18
|
+
.arg('--defaults', 'Include built-in Millas framework strings')
|
|
19
|
+
.arg('--fresh', 'Clear namespace files and rebuild from scratch')
|
|
20
|
+
.arg('--all', 'Publish to every locale in lang/')
|
|
21
|
+
.arg('--list', 'List available locales and exit')
|
|
22
|
+
.arg('--dry-run', 'Preview changes without writing')
|
|
23
|
+
.arg('--format',v=>v.string(), 'File format: js or json (default: js)')
|
|
24
|
+
.arg('--src',v=>v.string(), 'Extra directory to scan')
|
|
25
|
+
.description('Extract _() strings from app/ and write to lang/<locale>/<namespace>.js');
|
|
26
|
+
|
|
27
|
+
register
|
|
28
|
+
.command(this.missing)
|
|
29
|
+
.arg('locale', 'Target locale to check')
|
|
30
|
+
.description('Show untranslated keys in locale files');
|
|
31
|
+
|
|
32
|
+
register
|
|
33
|
+
.command(this.stats)
|
|
34
|
+
.description('Show translation completion % for all locales');
|
|
35
|
+
|
|
36
|
+
register
|
|
37
|
+
.command(this.keys)
|
|
38
|
+
.arg('--src',v=>v.string(), 'Extra directory to scan')
|
|
39
|
+
.description('List all _() keys found in source files, grouped by namespace');
|
|
40
|
+
}
|
|
60
41
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
if (!targets.length) {
|
|
66
|
-
console.log(chalk.yellow('\n No locales in lang/. Run: millas lang:publish <locale>\n'));
|
|
67
|
-
return;
|
|
68
|
-
}
|
|
69
|
-
} else if (locale) {
|
|
70
|
-
targets = [locale];
|
|
71
|
-
} else {
|
|
72
|
-
console.log(chalk.red('\n ✖ Specify a locale: millas lang:publish sw\n'));
|
|
73
|
-
console.log(chalk.gray(' millas lang:publish sw'));
|
|
74
|
-
console.log(chalk.gray(' millas lang:publish sw auth'));
|
|
75
|
-
console.log(chalk.gray(' millas lang:publish sw --defaults'));
|
|
76
|
-
console.log(chalk.gray(' millas lang:publish --all\n'));
|
|
77
|
-
process.exit(1);
|
|
78
|
-
}
|
|
42
|
+
async publish(locale, namespace, defaults, fresh, all, list, dryRun, format, src) {
|
|
43
|
+
const langPath = path.join(this.cwd, 'lang');
|
|
44
|
+
const fmt = format || 'js';
|
|
45
|
+
const srcDirs = this.#resolveScanDirs(src);
|
|
79
46
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
// ── Step 2: Merge framework defaults if -d ────────────────────────
|
|
86
|
-
if (options.defaults) {
|
|
87
|
-
const defaults = require('../i18n/defaults');
|
|
88
|
-
let added = 0;
|
|
89
|
-
for (const [key, val] of Object.entries(defaults)) {
|
|
90
|
-
if (!appKeys.has(key)) {
|
|
91
|
-
const { namespace: ns, bare } = parseKey(key);
|
|
92
|
-
appKeys.set(key, {
|
|
93
|
-
namespace: ns,
|
|
94
|
-
bare,
|
|
95
|
-
plural: Array.isArray(val) ? val[1] : null,
|
|
96
|
-
locations: ['[millas]'],
|
|
97
|
-
});
|
|
98
|
-
added++;
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
if (added) console.log(chalk.gray(` + ${added} framework string${added !== 1 ? 's' : ''} (-d)`));
|
|
102
|
-
}
|
|
47
|
+
if (list) {
|
|
48
|
+
this.#handleList(langPath);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
103
51
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
console.log(chalk.gray(' Use __(), __("ns::key"), _n(), _p(), _f() in your source files.\n'));
|
|
107
|
-
return;
|
|
108
|
-
}
|
|
52
|
+
const targets = this.#resolveTargets(locale, all, langPath);
|
|
53
|
+
if (!targets) return;
|
|
109
54
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
const nsNames = [...grouped.keys()].sort();
|
|
114
|
-
|
|
115
|
-
console.log(chalk.cyan(
|
|
116
|
-
` ${appKeys.size} string${appKeys.size !== 1 ? 's' : ''} → ` +
|
|
117
|
-
`${nsNames.length} namespace${nsNames.length !== 1 ? 's' : ''}: ${nsNames.join(', ')}\n`
|
|
118
|
-
));
|
|
119
|
-
|
|
120
|
-
fs.mkdirSync(langPath, { recursive: true });
|
|
121
|
-
let totalAdded = 0;
|
|
122
|
-
|
|
123
|
-
// ── Step 4: Write each locale × namespace combination ─────────────
|
|
124
|
-
for (const loc of targets) {
|
|
125
|
-
const locDir = path.join(langPath, loc);
|
|
126
|
-
fs.mkdirSync(locDir, { recursive: true });
|
|
127
|
-
|
|
128
|
-
let locAdded = 0;
|
|
129
|
-
const results = [];
|
|
130
|
-
|
|
131
|
-
for (const [ns, nsKeys] of grouped) {
|
|
132
|
-
const filePath = path.join(locDir, `${ns}.${fmt}`);
|
|
133
|
-
const isNew = !fs.existsSync(filePath);
|
|
134
|
-
const existing = (options.fresh || isNew) ? {} : loadCatalogue(filePath);
|
|
135
|
-
const added = [];
|
|
136
|
-
const kept = [];
|
|
137
|
-
|
|
138
|
-
for (const { bare, plural } of nsKeys.values()) {
|
|
139
|
-
if (!options.fresh && existing[bare] !== undefined) { kept.push(bare); continue; }
|
|
140
|
-
existing[bare] = loc === 'en'
|
|
141
|
-
? (plural ? [bare, plural] : bare)
|
|
142
|
-
: (plural ? [null, null] : null);
|
|
143
|
-
added.push(bare);
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
if (added.length > 0 && !options.dryRun) {
|
|
147
|
-
writeCatalogue(filePath, existing, fmt);
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
locAdded += added.length;
|
|
151
|
-
totalAdded += added.length;
|
|
152
|
-
results.push({ ns, filePath: path.relative(cwd, filePath), added, kept, isNew });
|
|
153
|
-
}
|
|
55
|
+
const relDirs = srcDirs.map(d => path.relative(this.cwd, d)).filter(Boolean);
|
|
56
|
+
this.logger.log(this.style.secondary(`\n Scanning: ${relDirs.join(', ')}...`));
|
|
57
|
+
const appKeys = this.#extractKeysFromDirs(srcDirs);
|
|
154
58
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
59
|
+
if (defaults) {
|
|
60
|
+
const added = this.#mergeDefaults(appKeys);
|
|
61
|
+
if (added) this.logger.log(this.style.secondary(` + ${added} framework string${added !== 1 ? 's' : ''} (-d)`));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (!appKeys.size) {
|
|
65
|
+
this.warn('No translatable strings found.');
|
|
66
|
+
this.logger.log(this.style.secondary(' Use __(), __("ns::key"), _n(), _p(), _f() in your source files.\n'));
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const grouped = this.#groupByNamespace(appKeys, namespace || null);
|
|
71
|
+
const nsNames = [...grouped.keys()].sort();
|
|
72
|
+
|
|
73
|
+
this.logger.log(this.style.info(
|
|
74
|
+
` ${appKeys.size} string${appKeys.size !== 1 ? 's' : ''} → ` +
|
|
75
|
+
`${nsNames.length} namespace${nsNames.length !== 1 ? 's' : ''}: ${nsNames.join(', ')}\n`
|
|
76
|
+
));
|
|
77
|
+
|
|
78
|
+
fs.mkdirSync(langPath, { recursive: true });
|
|
79
|
+
let totalAdded = 0;
|
|
80
|
+
|
|
81
|
+
for (const loc of targets) {
|
|
82
|
+
const locDir = path.join(langPath, loc);
|
|
83
|
+
fs.mkdirSync(locDir, { recursive: true });
|
|
84
|
+
|
|
85
|
+
let locAdded = 0;
|
|
86
|
+
const results = [];
|
|
87
|
+
|
|
88
|
+
for (const [ns, nsKeys] of grouped) {
|
|
89
|
+
const filePath = path.join(locDir, `${ns}.${fmt}`);
|
|
90
|
+
const isNew = !fs.existsSync(filePath);
|
|
91
|
+
const existing = (fresh || isNew) ? {} : this.#loadCatalogue(filePath);
|
|
92
|
+
const added = [];
|
|
93
|
+
const kept = [];
|
|
94
|
+
|
|
95
|
+
for (const { bare, plural } of nsKeys.values()) {
|
|
96
|
+
if (!fresh && existing[bare] !== undefined) { kept.push(bare); continue; }
|
|
97
|
+
existing[bare] = loc === 'en'
|
|
98
|
+
? (plural ? [bare, plural] : bare)
|
|
99
|
+
: (plural ? [null, null] : null);
|
|
100
|
+
added.push(bare);
|
|
169
101
|
}
|
|
170
102
|
|
|
171
|
-
if (
|
|
172
|
-
|
|
173
|
-
} else if (totalAdded > 0) {
|
|
174
|
-
console.log(chalk.green(`\n Done. Fill in null values in lang/ to add translations.`));
|
|
175
|
-
console.log(chalk.gray(' Run millas lang:missing to see what needs translation.\n'));
|
|
176
|
-
} else {
|
|
177
|
-
console.log(chalk.gray('\n All files already up to date.\n'));
|
|
103
|
+
if (added.length > 0 && !dryRun) {
|
|
104
|
+
this.#writeCatalogue(filePath, existing, fmt);
|
|
178
105
|
}
|
|
179
106
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
process.exit(1);
|
|
107
|
+
locAdded += added.length;
|
|
108
|
+
totalAdded += added.length;
|
|
109
|
+
results.push({ ns, filePath: path.relative(this.cwd, filePath), added, kept, isNew });
|
|
184
110
|
}
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
// ── lang:missing [locale] ────────────────────────────────────────────────
|
|
188
|
-
program
|
|
189
|
-
.command('lang:missing [locale]')
|
|
190
|
-
.description('Show untranslated keys in locale files')
|
|
191
|
-
.action(async (locale) => {
|
|
192
|
-
try {
|
|
193
|
-
const langPath = path.join(process.cwd(), 'lang');
|
|
194
|
-
if (!fs.existsSync(langPath)) {
|
|
195
|
-
console.log(chalk.yellow('\n lang/ not found. Run: millas lang:publish <locale>\n'));
|
|
196
|
-
return;
|
|
197
|
-
}
|
|
198
111
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
112
|
+
if (dryRun) {
|
|
113
|
+
this.logger.log(this.style.info(` ${loc} — dry run:`));
|
|
114
|
+
results.forEach(r => {
|
|
115
|
+
const tag = r.isNew ? this.style.info('(new)') : this.style.secondary(`(${r.kept.length} kept)`);
|
|
116
|
+
this.logger.log(` ${r.ns.padEnd(14)} ${tag} +${r.added.length} keys → ${r.filePath}`);
|
|
117
|
+
});
|
|
118
|
+
} else {
|
|
119
|
+
const action = fresh ? 'Rebuilt' : (results.every(r => r.isNew) ? 'Created' : 'Updated');
|
|
120
|
+
this.logger.log(this.style.success(` ✔ ${loc.padEnd(10)}`) + ` ${action} — ${locAdded} key${locAdded !== 1 ? 's' : ''} added`);
|
|
121
|
+
results.filter(r => r.added.length > 0).forEach(r => {
|
|
122
|
+
this.logger.log(this.style.secondary(` ${r.ns.padEnd(14)} +${r.added.length} → ${r.filePath}`));
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
}
|
|
202
126
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
127
|
+
if (dryRun) {
|
|
128
|
+
this.warn('Dry run — nothing written.');
|
|
129
|
+
} else if (totalAdded > 0) {
|
|
130
|
+
this.success('Done. Fill in null values in lang/ to add translations.');
|
|
131
|
+
this.logger.log(this.style.secondary(' Run millas lang:missing to see what needs translation.\n'));
|
|
132
|
+
} else {
|
|
133
|
+
this.logger.log(this.style.secondary('\n All files already up to date.\n'));
|
|
134
|
+
}
|
|
135
|
+
}
|
|
209
136
|
|
|
210
|
-
|
|
211
|
-
|
|
137
|
+
async missing(locale) {
|
|
138
|
+
const langPath = path.join(this.cwd, 'lang');
|
|
139
|
+
if (!fs.existsSync(langPath)) {
|
|
140
|
+
this.warn('lang/ not found. Run: millas lang:publish <locale>');
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
212
143
|
|
|
213
|
-
|
|
214
|
-
|
|
144
|
+
const targets = this.#readLocales(langPath)
|
|
145
|
+
.filter(l => l !== 'en')
|
|
146
|
+
.filter(l => !locale || l === locale);
|
|
215
147
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
148
|
+
if (!targets.length) {
|
|
149
|
+
this.warn(locale
|
|
150
|
+
? `Locale "${locale}" not found in lang/.`
|
|
151
|
+
: 'No non-English locales found.');
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
219
154
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
const val = locCat[key];
|
|
224
|
-
if (val === null || val === undefined || (Array.isArray(val) && val.some(v => v === null))) {
|
|
225
|
-
missing.push(`${ns}::${key}`);
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
}
|
|
155
|
+
const enKeys = this.#loadAllNamespaces(langPath, 'en');
|
|
156
|
+
let totalMissing = 0;
|
|
157
|
+
this.logger.log('');
|
|
229
158
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
totalMissing += missing.length;
|
|
159
|
+
for (const loc of targets) {
|
|
160
|
+
const locKeys = this.#loadAllNamespaces(langPath, loc);
|
|
161
|
+
const missing = [];
|
|
234
162
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
163
|
+
for (const [ns, enCat] of enKeys) {
|
|
164
|
+
const locCat = locKeys.get(ns) || {};
|
|
165
|
+
for (const key of Object.keys(enCat)) {
|
|
166
|
+
const val = locCat[key];
|
|
167
|
+
if (val === null || val === undefined || (Array.isArray(val) && val.some(v => v === null))) {
|
|
168
|
+
missing.push(`${ns}::${key}`);
|
|
240
169
|
}
|
|
241
170
|
}
|
|
171
|
+
}
|
|
242
172
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
173
|
+
const total = [...enKeys.values()].reduce((s, c) => s + Object.keys(c).length, 0);
|
|
174
|
+
const pct = total ? Math.round(((total - missing.length) / total) * 100) : 100;
|
|
175
|
+
const color = pct === 100 ? this.style.success : pct >= 50 ? this.style.warning : this.style.danger;
|
|
176
|
+
totalMissing += missing.length;
|
|
247
177
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
178
|
+
if (!missing.length) {
|
|
179
|
+
this.logger.log(color(` ${loc.padEnd(10)} ${this.#progressBar(pct, 18)} 100% Fully translated`));
|
|
180
|
+
} else {
|
|
181
|
+
this.logger.log(color(` ${loc.padEnd(10)} ${this.#progressBar(pct, 18)} ${pct}% ${missing.length} missing:`));
|
|
182
|
+
missing.forEach(k => this.logger.log(this.style.secondary(` - ${JSON.stringify(k)}`)));
|
|
251
183
|
}
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
// ── lang:stats ───────────────────────────────────────────────────────────
|
|
255
|
-
program
|
|
256
|
-
.command('lang:stats')
|
|
257
|
-
.description('Show translation completion % for all locales')
|
|
258
|
-
.action(async () => {
|
|
259
|
-
try {
|
|
260
|
-
const langPath = path.join(process.cwd(), 'lang');
|
|
261
|
-
if (!fs.existsSync(langPath)) {
|
|
262
|
-
console.log(chalk.yellow('\n lang/ not found. Run: millas lang:publish <locale>\n'));
|
|
263
|
-
return;
|
|
264
|
-
}
|
|
184
|
+
}
|
|
265
185
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
186
|
+
this.logger.log('');
|
|
187
|
+
this.logger.log(totalMissing > 0
|
|
188
|
+
? this.style.warning(` ${totalMissing} key${totalMissing !== 1 ? 's' : ''} need translation.\n`)
|
|
189
|
+
: this.style.success(' All locales fully translated.\n'));
|
|
190
|
+
}
|
|
269
191
|
|
|
270
|
-
|
|
271
|
-
|
|
192
|
+
async stats() {
|
|
193
|
+
const langPath = path.join(this.cwd, 'lang');
|
|
194
|
+
if (!fs.existsSync(langPath)) {
|
|
195
|
+
this.warn('lang/ not found. Run: millas lang:publish <locale>');
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
272
198
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
let translated = 0;
|
|
280
|
-
for (const [ns, enCat] of enKeys) {
|
|
281
|
-
const locCat = locKeys.get(ns) || {};
|
|
282
|
-
for (const key of Object.keys(enCat)) {
|
|
283
|
-
const v = locCat[key];
|
|
284
|
-
if (v !== null && v !== undefined && (!Array.isArray(v) || v.every(x => x !== null))) translated++;
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
const pct = total > 0 ? Math.round((translated / total) * 100) : 0;
|
|
288
|
-
const color = pct === 100 ? chalk.green : pct >= 50 ? chalk.yellow : chalk.red;
|
|
289
|
-
console.log(` ${chalk.bold(loc.padEnd(10))} ${color(progressBar(pct, 18))} ${color((pct + '%').padStart(4))} ${translated}/${total}`);
|
|
290
|
-
}
|
|
291
|
-
console.log('');
|
|
199
|
+
const locales = this.#readLocales(langPath);
|
|
200
|
+
const enKeys = this.#loadAllNamespaces(langPath, 'en');
|
|
201
|
+
const total = [...enKeys.values()].reduce((s, c) => s + Object.keys(c).length, 0);
|
|
202
|
+
|
|
203
|
+
this.logger.log(this.style.info(`\n Translation Stats (source: en, ${total} key${total !== 1 ? 's' : ''})\n`));
|
|
204
|
+
this.logger.log(this.style.secondary(' ' + '─'.repeat(55)));
|
|
292
205
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
206
|
+
for (const loc of locales.sort()) {
|
|
207
|
+
if (loc === 'en') {
|
|
208
|
+
this.logger.log(` ${this.style.bold(loc.padEnd(10))} ${this.style.success(this.#progressBar(100, 18))} ${this.style.success('100%')} ${total}/${total} ${this.style.secondary('(source)')}`);
|
|
209
|
+
continue;
|
|
296
210
|
}
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
.action(async (options) => {
|
|
305
|
-
try {
|
|
306
|
-
const cwd = process.cwd();
|
|
307
|
-
const keys = extractKeysFromDirs(resolveScanDirs(cwd, options.src), cwd);
|
|
308
|
-
|
|
309
|
-
if (!keys.size) {
|
|
310
|
-
console.log(chalk.yellow(`\n No translatable strings found.\n`));
|
|
311
|
-
return;
|
|
211
|
+
const locKeys = this.#loadAllNamespaces(langPath, loc);
|
|
212
|
+
let translated = 0;
|
|
213
|
+
for (const [ns, enCat] of enKeys) {
|
|
214
|
+
const locCat = locKeys.get(ns) || {};
|
|
215
|
+
for (const key of Object.keys(enCat)) {
|
|
216
|
+
const v = locCat[key];
|
|
217
|
+
if (v !== null && v !== undefined && (!Array.isArray(v) || v.every(x => x !== null))) translated++;
|
|
312
218
|
}
|
|
219
|
+
}
|
|
220
|
+
const pct = total > 0 ? Math.round((translated / total) * 100) : 0;
|
|
221
|
+
const color = pct === 100 ? this.style.success : pct >= 50 ? this.style.warning : this.style.danger;
|
|
222
|
+
this.logger.log(` ${this.style.bold(loc.padEnd(10))} ${color(this.#progressBar(pct, 18))} ${color((pct + '%').padStart(4))} ${translated}/${total}`);
|
|
223
|
+
}
|
|
224
|
+
this.logger.log('');
|
|
225
|
+
}
|
|
313
226
|
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
console.log(chalk.cyan(`\n ${keys.size} string${keys.size !== 1 ? 's' : ''} across ${grouped.size} namespace${grouped.size !== 1 ? 's' : ''}:\n`));
|
|
317
|
-
|
|
318
|
-
for (const [ns, nsKeys] of [...grouped].sort((a, b) => a[0].localeCompare(b[0]))) {
|
|
319
|
-
console.log(chalk.bold(` [${ns}]`));
|
|
320
|
-
for (const [key, meta] of [...nsKeys].sort((a, b) => a[0].localeCompare(b[0]))) {
|
|
321
|
-
const p = meta.plural ? chalk.gray(` [plural: ${JSON.stringify(meta.plural)}]`) : '';
|
|
322
|
-
const locs = meta.locations.slice(0, 2).map(l => chalk.gray(l)).join(', ');
|
|
323
|
-
const more = meta.locations.length > 2 ? chalk.gray(` +${meta.locations.length - 2}`) : '';
|
|
324
|
-
console.log(` ${chalk.white(JSON.stringify(meta.bare))}${p}`);
|
|
325
|
-
console.log(` ${locs}${more}`);
|
|
326
|
-
}
|
|
327
|
-
console.log('');
|
|
328
|
-
}
|
|
227
|
+
async keys(src) {
|
|
228
|
+
const keys = this.#extractKeysFromDirs(this.#resolveScanDirs(src));
|
|
329
229
|
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
230
|
+
if (!keys.size) {
|
|
231
|
+
this.warn('No translatable strings found.');
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const grouped = this.#groupByNamespace(keys, null);
|
|
236
|
+
this.logger.log(this.style.info(`\n ${keys.size} string${keys.size !== 1 ? 's' : ''} across ${grouped.size} namespace${grouped.size !== 1 ? 's' : ''}:\n`));
|
|
237
|
+
|
|
238
|
+
for (const [ns, nsKeys] of [...grouped].sort((a, b) => a[0].localeCompare(b[0]))) {
|
|
239
|
+
this.logger.log(this.style.bold(` [${ns}]`));
|
|
240
|
+
for (const [key, meta] of [...nsKeys].sort((a, b) => a[0].localeCompare(b[0]))) {
|
|
241
|
+
const p = meta.plural ? this.style.secondary(` [plural: ${JSON.stringify(meta.plural)}]`) : '';
|
|
242
|
+
const locs = meta.locations.slice(0, 2).map(l => this.style.secondary(l)).join(', ');
|
|
243
|
+
const more = meta.locations.length > 2 ? this.style.secondary(` +${meta.locations.length - 2}`) : '';
|
|
244
|
+
this.logger.log(` ${this.style.light(JSON.stringify(meta.bare))}${p}`);
|
|
245
|
+
this.logger.log(` ${locs}${more}`);
|
|
333
246
|
}
|
|
334
|
-
|
|
335
|
-
};
|
|
336
|
-
|
|
337
|
-
// ─── Directory resolution ────────────────────────────────────────────────────────
|
|
338
|
-
|
|
339
|
-
/**
|
|
340
|
-
* Resolve the list of directories to scan for translatable strings.
|
|
341
|
-
*
|
|
342
|
-
* Priority:
|
|
343
|
-
* 1. config/i18n.js → scan: ['app', 'resources', 'routes', 'mail']
|
|
344
|
-
* 2. --src <dir> CLI flag (adds to defaults, does not replace them)
|
|
345
|
-
* 3. Built-in defaults: app/, resources/, routes/, mail/
|
|
346
|
-
*
|
|
347
|
-
* Only returns directories that actually exist — silently skips missing ones
|
|
348
|
-
* so projects that don't have a resources/ directory don't see warnings.
|
|
349
|
-
*/
|
|
350
|
-
function resolveScanDirs(cwd, extraSrc) {
|
|
351
|
-
// Try config/i18n.js first
|
|
352
|
-
let configured = null;
|
|
353
|
-
try {
|
|
354
|
-
const i18nConfig = require(path.join(cwd, 'config/i18n'));
|
|
355
|
-
if (Array.isArray(i18nConfig.scan) && i18nConfig.scan.length) {
|
|
356
|
-
configured = i18nConfig.scan;
|
|
247
|
+
this.logger.log('');
|
|
357
248
|
}
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
const rawDirs = configured || [
|
|
361
|
-
'app', // controllers, models, services, middleware, jobs
|
|
362
|
-
'resources', // views (.njk, .html) — resources/views/ is common
|
|
363
|
-
'routes', // route files often contain inline messages
|
|
364
|
-
'mail', // email templates
|
|
365
|
-
'providers', // service providers sometimes have translatable strings
|
|
366
|
-
];
|
|
367
|
-
|
|
368
|
-
// Add any --src flag value on top
|
|
369
|
-
if (extraSrc) rawDirs.push(extraSrc);
|
|
370
|
-
|
|
371
|
-
// Resolve to absolute paths and filter to only existing ones
|
|
372
|
-
return [...new Set(rawDirs)]
|
|
373
|
-
.map(d => path.isAbsolute(d) ? d : path.join(cwd, d))
|
|
374
|
-
.filter(d => fs.existsSync(d));
|
|
375
|
-
}
|
|
249
|
+
}
|
|
376
250
|
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
for (const loc of meta.locations) {
|
|
392
|
-
if (!existing.locations.includes(loc)) existing.locations.push(loc);
|
|
393
|
-
}
|
|
251
|
+
#handleList(langPath) {
|
|
252
|
+
if (!fs.existsSync(langPath)) {
|
|
253
|
+
this.warn('lang/ not found — nothing published yet.');
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
const locales = this.#readLocales(langPath);
|
|
257
|
+
if (!locales.length) {
|
|
258
|
+
this.warn('No locale files in lang/ yet.');
|
|
259
|
+
} else {
|
|
260
|
+
this.logger.log(this.style.info('\n Available locales:\n'));
|
|
261
|
+
for (const loc of locales) {
|
|
262
|
+
const nsFiles = this.#readNamespaces(langPath, loc);
|
|
263
|
+
const nsList = nsFiles.length ? this.style.secondary(` (${nsFiles.join(', ')})`) : '';
|
|
264
|
+
this.logger.log(` ${this.style.light(loc)}${nsList}`);
|
|
394
265
|
}
|
|
266
|
+
this.logger.log('');
|
|
395
267
|
}
|
|
396
268
|
}
|
|
397
|
-
return merged;
|
|
398
|
-
}
|
|
399
269
|
|
|
400
|
-
|
|
270
|
+
#resolveTargets(locale, all, langPath) {
|
|
271
|
+
if (all) {
|
|
272
|
+
const targets = this.#readLocales(langPath);
|
|
273
|
+
if (!targets.length) {
|
|
274
|
+
this.warn('No locales in lang/. Run: millas lang:publish <locale>');
|
|
275
|
+
return null;
|
|
276
|
+
}
|
|
277
|
+
return targets;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (locale) {
|
|
281
|
+
return [locale];
|
|
282
|
+
}
|
|
401
283
|
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
284
|
+
this.error('Specify a locale: millas lang:publish sw');
|
|
285
|
+
this.logger.log(this.style.secondary(' millas lang:publish sw'));
|
|
286
|
+
this.logger.log(this.style.secondary(' millas lang:publish sw auth'));
|
|
287
|
+
this.logger.log(this.style.secondary(' millas lang:publish sw --defaults'));
|
|
288
|
+
this.logger.log(this.style.secondary(' millas lang:publish --all\n'));
|
|
289
|
+
throw new Error('Locale required');
|
|
290
|
+
}
|
|
407
291
|
|
|
408
|
-
|
|
292
|
+
#resolveScanDirs(extraSrc) {
|
|
293
|
+
let configured = null;
|
|
294
|
+
try {
|
|
295
|
+
const i18nConfig = require(path.join(this.cwd, 'config/i18n'));
|
|
296
|
+
if (Array.isArray(i18nConfig.scan) && i18nConfig.scan.length) {
|
|
297
|
+
configured = i18nConfig.scan;
|
|
298
|
+
}
|
|
299
|
+
} catch { }
|
|
409
300
|
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
* Returns Map<fullKey, { namespace, bare, plural, locations[] }>
|
|
413
|
-
* fullKey is the original string including namespace prefix if present.
|
|
414
|
-
*/
|
|
415
|
-
function extractKeys(srcPath, cwd) {
|
|
416
|
-
const keys = new Map();
|
|
417
|
-
if (!fs.existsSync(srcPath)) return keys;
|
|
301
|
+
const rawDirs = configured || ['app', 'resources', 'routes', 'mail', 'providers'];
|
|
302
|
+
if (extraSrc) rawDirs.push(extraSrc);
|
|
418
303
|
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
304
|
+
return [...new Set(rawDirs)]
|
|
305
|
+
.map(d => path.isAbsolute(d) ? d : path.join(this.cwd, d))
|
|
306
|
+
.filter(d => fs.existsSync(d));
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
#extractKeysFromDirs(dirs) {
|
|
310
|
+
const merged = new Map();
|
|
311
|
+
for (const dir of dirs) {
|
|
312
|
+
const keys = this.#extractKeys(dir);
|
|
313
|
+
for (const [fullKey, meta] of keys) {
|
|
314
|
+
if (!merged.has(fullKey)) {
|
|
315
|
+
merged.set(fullKey, { ...meta, locations: [...meta.locations] });
|
|
316
|
+
} else {
|
|
317
|
+
const existing = merged.get(fullKey);
|
|
318
|
+
for (const loc of meta.locations) {
|
|
319
|
+
if (!existing.locations.includes(loc)) existing.locations.push(loc);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
return merged;
|
|
325
|
+
}
|
|
423
326
|
|
|
424
|
-
|
|
425
|
-
|
|
327
|
+
#extractKeys(srcPath) {
|
|
328
|
+
const keys = new Map();
|
|
329
|
+
if (!fs.existsSync(srcPath)) return keys;
|
|
426
330
|
|
|
427
|
-
|
|
428
|
-
|
|
331
|
+
for (const filePath of this.#walkFiles(srcPath, ['.js', '.njk', '.html'])) {
|
|
332
|
+
let source;
|
|
333
|
+
try { source = fs.readFileSync(filePath, 'utf8'); } catch { continue; }
|
|
334
|
+
const rel = path.relative(this.cwd, filePath);
|
|
429
335
|
|
|
430
|
-
|
|
431
|
-
|
|
336
|
+
this.#scanSingle(source, /\b__\(\s*(['\"`])((?:\\.|(?!\1).)*)\1/g, 2, null, rel, keys);
|
|
337
|
+
this.#scanSingle(source, /\b_f\(\s*(['\"`])((?:\\.|(?!\1).)*)\1/g, 2, null, rel, keys);
|
|
338
|
+
this.#scanPlural(source, /\b_n\(\s*(['\"`])((?:\\.|(?!\1).)*)\1\s*,\s*(['\"`])((?:\\.|(?!\3).)*)\3/g, rel, keys);
|
|
339
|
+
this.#scanPlural(source, /\b_fn\(\s*(['\"`])((?:\\.|(?!\1).)*)\1\s*,\s*(['\"`])((?:\\.|(?!\3).)*)\3/g, rel, keys);
|
|
432
340
|
|
|
433
|
-
|
|
434
|
-
|
|
341
|
+
let m;
|
|
342
|
+
const pPat = /\b_p\(\s*(['\"`])((?:\\.|(?!\1).)*)\1\s*,\s*(['\"`])((?:\\.|(?!\3).)*)\3/g;
|
|
343
|
+
while ((m = pPat.exec(source)) !== null) {
|
|
344
|
+
this.#addKey(keys, `${this.#unescape(m[2])}|${this.#unescape(m[4])}`, null, rel);
|
|
345
|
+
}
|
|
435
346
|
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
const pPat = /\b_p\(\s*(['"`])((?:\\.|(?!\1).)*)\1\s*,\s*(['"`])((?:\\.|(?!\3).)*)\3/g;
|
|
439
|
-
while ((m = pPat.exec(source)) !== null) {
|
|
440
|
-
addKey(keys, `${un(m[2])}|${un(m[4])}`, null, rel);
|
|
347
|
+
this.#scanSingle(source, /\{\{[^}]*(['\"])((?:\\.|(?!\1).)*)\1\s*\|\s*__/g, 2, null, rel, keys);
|
|
348
|
+
this.#scanSingle(source, /\{\{[^}]*(['\"])((?:\\.|(?!\1).)*)\1\s*\|\s*_f/g, 2, null, rel, keys);
|
|
441
349
|
}
|
|
442
350
|
|
|
443
|
-
|
|
444
|
-
scanSingle(source, /\{\{[^}]*(['"])((?:\\.|(?!\1).)*)\1\s*\|\s*__/g, 2, null, rel, keys);
|
|
445
|
-
scanSingle(source, /\{\{[^}]*(['"])((?:\\.|(?!\1).)*)\1\s*\|\s*_f/g, 2, null, rel, keys);
|
|
351
|
+
return keys;
|
|
446
352
|
}
|
|
447
353
|
|
|
448
|
-
|
|
449
|
-
|
|
354
|
+
#scanSingle(source, regex, keyGroup, _pluralGroup, location, keys) {
|
|
355
|
+
let m;
|
|
356
|
+
while ((m = regex.exec(source)) !== null) this.#addKey(keys, this.#unescape(m[keyGroup]), null, location);
|
|
357
|
+
}
|
|
450
358
|
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
}
|
|
359
|
+
#scanPlural(source, regex, location, keys) {
|
|
360
|
+
let m;
|
|
361
|
+
while ((m = regex.exec(source)) !== null) this.#addKey(keys, this.#unescape(m[2]), this.#unescape(m[4]), location);
|
|
362
|
+
}
|
|
455
363
|
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
}
|
|
364
|
+
#addKey(keys, fullKey, plural, location) {
|
|
365
|
+
if (!fullKey || !fullKey.trim()) return;
|
|
366
|
+
const { namespace, bare } = this.#parseKey(fullKey);
|
|
367
|
+
if (!keys.has(fullKey)) keys.set(fullKey, { namespace, bare, plural: null, locations: [] });
|
|
368
|
+
const e = keys.get(fullKey);
|
|
369
|
+
if (!e.plural && plural) e.plural = plural;
|
|
370
|
+
if (!e.locations.includes(location)) e.locations.push(location);
|
|
371
|
+
}
|
|
460
372
|
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
if (!e.plural && plural) e.plural = plural;
|
|
467
|
-
if (!e.locations.includes(location)) e.locations.push(location);
|
|
468
|
-
}
|
|
373
|
+
#unescape(s) {
|
|
374
|
+
return String(s)
|
|
375
|
+
.replace(/\\n/g, '\n').replace(/\\t/g, '\t')
|
|
376
|
+
.replace(/\\'/g, "'").replace(/\\"/g, '"').replace(/\\\\/g, '\\');
|
|
377
|
+
}
|
|
469
378
|
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
.
|
|
474
|
-
}
|
|
379
|
+
#parseKey(key) {
|
|
380
|
+
const sep = key.indexOf('::');
|
|
381
|
+
if (sep === -1) return { namespace: DEFAULT_NS, bare: key };
|
|
382
|
+
return { namespace: key.slice(0, sep).trim() || DEFAULT_NS, bare: key.slice(sep + 2) };
|
|
383
|
+
}
|
|
475
384
|
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
if (nsFilter && ns !== nsFilter) continue;
|
|
486
|
-
if (!groups.has(ns)) groups.set(ns, new Map());
|
|
487
|
-
groups.get(ns).set(fullKey, meta);
|
|
385
|
+
#groupByNamespace(keys, nsFilter) {
|
|
386
|
+
const groups = new Map();
|
|
387
|
+
for (const [fullKey, meta] of keys) {
|
|
388
|
+
const ns = meta.namespace;
|
|
389
|
+
if (nsFilter && ns !== nsFilter) continue;
|
|
390
|
+
if (!groups.has(ns)) groups.set(ns, new Map());
|
|
391
|
+
groups.get(ns).set(fullKey, meta);
|
|
392
|
+
}
|
|
393
|
+
return groups;
|
|
488
394
|
}
|
|
489
|
-
return groups;
|
|
490
|
-
}
|
|
491
395
|
|
|
492
|
-
|
|
396
|
+
#mergeDefaults(appKeys) {
|
|
397
|
+
const defaults = require('../i18n/defaults');
|
|
398
|
+
let added = 0;
|
|
399
|
+
for (const [key, val] of Object.entries(defaults)) {
|
|
400
|
+
if (!appKeys.has(key)) {
|
|
401
|
+
const { namespace: ns, bare } = this.#parseKey(key);
|
|
402
|
+
appKeys.set(key, {
|
|
403
|
+
namespace: ns,
|
|
404
|
+
bare,
|
|
405
|
+
plural: Array.isArray(val) ? val[1] : null,
|
|
406
|
+
locations: ['[millas]'],
|
|
407
|
+
});
|
|
408
|
+
added++;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
return added;
|
|
412
|
+
}
|
|
493
413
|
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
}
|
|
414
|
+
#loadCatalogue(filePath) {
|
|
415
|
+
if (!fs.existsSync(filePath)) return {};
|
|
416
|
+
try {
|
|
417
|
+
try { delete require.cache[require.resolve(filePath)]; } catch { }
|
|
418
|
+
return filePath.endsWith('.json') ? JSON.parse(fs.readFileSync(filePath, 'utf8')) : (require(filePath) || {});
|
|
419
|
+
} catch { return {}; }
|
|
420
|
+
}
|
|
501
421
|
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
const
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
for (const ext of ['.js', '.json']) {
|
|
520
|
-
const flat = path.join(langPath, locale + ext);
|
|
521
|
-
if (fs.existsSync(flat)) {
|
|
522
|
-
result.set(DEFAULT_NS, loadCatalogue(flat));
|
|
523
|
-
break;
|
|
422
|
+
#loadAllNamespaces(langPath, locale) {
|
|
423
|
+
const result = new Map();
|
|
424
|
+
const locDir = path.join(langPath, locale);
|
|
425
|
+
|
|
426
|
+
if (fs.existsSync(locDir) && fs.statSync(locDir).isDirectory()) {
|
|
427
|
+
for (const file of fs.readdirSync(locDir).filter(f => /\.(js|json)$/.test(f))) {
|
|
428
|
+
const ns = file.replace(/\.(js|json)$/, '');
|
|
429
|
+
const cat = this.#loadCatalogue(path.join(locDir, file));
|
|
430
|
+
result.set(ns, cat);
|
|
431
|
+
}
|
|
432
|
+
} else {
|
|
433
|
+
for (const ext of ['.js', '.json']) {
|
|
434
|
+
const flat = path.join(langPath, locale + ext);
|
|
435
|
+
if (fs.existsSync(flat)) {
|
|
436
|
+
result.set(DEFAULT_NS, this.#loadCatalogue(flat));
|
|
437
|
+
break;
|
|
438
|
+
}
|
|
524
439
|
}
|
|
525
440
|
}
|
|
441
|
+
return result;
|
|
526
442
|
}
|
|
527
|
-
return result;
|
|
528
|
-
}
|
|
529
443
|
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
444
|
+
#writeCatalogue(filePath, catalogue, fmt) {
|
|
445
|
+
const sorted = Object.keys(catalogue).sort().reduce((a, k) => { a[k] = catalogue[k]; return a; }, {});
|
|
446
|
+
let content;
|
|
447
|
+
if (fmt === 'json') {
|
|
448
|
+
content = JSON.stringify(sorted, null, 2) + '\n';
|
|
449
|
+
} else {
|
|
450
|
+
const lines = ["'use strict';\n", 'module.exports = {'];
|
|
451
|
+
for (const [k, v] of Object.entries(sorted)) {
|
|
452
|
+
const key = JSON.stringify(k);
|
|
453
|
+
if (v === null) lines.push(` ${key}: null,`);
|
|
454
|
+
else if (Array.isArray(v)) lines.push(` ${key}: [${v.map(x => x === null ? 'null' : JSON.stringify(x)).join(', ')}],`);
|
|
455
|
+
else lines.push(` ${key}: ${JSON.stringify(v)},`);
|
|
456
|
+
}
|
|
457
|
+
lines.push('};\n');
|
|
458
|
+
content = lines.join('\n');
|
|
542
459
|
}
|
|
543
|
-
|
|
544
|
-
content
|
|
460
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
461
|
+
fs.writeFileSync(filePath, content, 'utf8');
|
|
545
462
|
}
|
|
546
|
-
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
547
|
-
fs.writeFileSync(filePath, content, 'utf8');
|
|
548
|
-
}
|
|
549
463
|
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
function readLocales(langPath) {
|
|
557
|
-
if (!fs.existsSync(langPath)) return [];
|
|
558
|
-
return fs.readdirSync(langPath, { withFileTypes: true })
|
|
559
|
-
.filter(e => (e.isFile() && /\.(js|json)$/.test(e.name)) || e.isDirectory())
|
|
560
|
-
.map(e => e.name.replace(/\.(js|json)$/, ''));
|
|
561
|
-
}
|
|
464
|
+
#readLocales(langPath) {
|
|
465
|
+
if (!fs.existsSync(langPath)) return [];
|
|
466
|
+
return fs.readdirSync(langPath, { withFileTypes: true })
|
|
467
|
+
.filter(e => (e.isFile() && /\.(js|json)$/.test(e.name)) || e.isDirectory())
|
|
468
|
+
.map(e => e.name.replace(/\.(js|json)$/, ''));
|
|
469
|
+
}
|
|
562
470
|
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
471
|
+
#readNamespaces(langPath, locale) {
|
|
472
|
+
const locDir = path.join(langPath, locale);
|
|
473
|
+
if (!fs.existsSync(locDir) || !fs.statSync(locDir).isDirectory()) return [];
|
|
474
|
+
return fs.readdirSync(locDir)
|
|
475
|
+
.filter(f => /\.(js|json)$/.test(f))
|
|
476
|
+
.map(f => f.replace(/\.(js|json)$/, ''));
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
#walkFiles(dir, exts) {
|
|
480
|
+
const results = [];
|
|
481
|
+
if (!fs.existsSync(dir)) return results;
|
|
482
|
+
for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
483
|
+
if (e.name.startsWith('.') || e.name === 'node_modules') continue;
|
|
484
|
+
const full = path.join(dir, e.name);
|
|
485
|
+
if (e.isDirectory()) results.push(...this.#walkFiles(full, exts));
|
|
486
|
+
else if (exts.some(x => e.name.endsWith(x))) results.push(full);
|
|
487
|
+
}
|
|
488
|
+
return results;
|
|
489
|
+
}
|
|
573
490
|
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
578
|
-
if (e.name.startsWith('.') || e.name === 'node_modules') continue;
|
|
579
|
-
const full = path.join(dir, e.name);
|
|
580
|
-
if (e.isDirectory()) results.push(...walkFiles(full, exts));
|
|
581
|
-
else if (exts.some(x => e.name.endsWith(x))) results.push(full);
|
|
491
|
+
#progressBar(pct, width) {
|
|
492
|
+
const f = Math.round((pct / 100) * width);
|
|
493
|
+
return '[' + '█'.repeat(f) + '░'.repeat(width - f) + ']';
|
|
582
494
|
}
|
|
583
|
-
return results;
|
|
584
495
|
}
|
|
585
496
|
|
|
586
|
-
|
|
587
|
-
const f = Math.round((pct / 100) * width);
|
|
588
|
-
return '[' + '█'.repeat(f) + '░'.repeat(width - f) + ']';
|
|
589
|
-
}
|
|
497
|
+
module.exports = LangCommand;
|