@saykit/config 0.0.0-beta-20260309151609
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/dist/commands/index.d.mts +1 -0
- package/dist/commands/index.mjs +579 -0
- package/dist/index.d.mts +163 -0
- package/dist/index.mjs +8 -0
- package/dist/schema.json +85 -0
- package/package.json +58 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { };
|
|
@@ -0,0 +1,579 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import { Command, program } from "@commander-js/extra-typings";
|
|
4
|
+
import { dirname, extname, join, relative, resolve } from "node:path";
|
|
5
|
+
import { pathToFileURL } from "node:url";
|
|
6
|
+
import { err, ok } from "neverthrow";
|
|
7
|
+
import picomatch from "picomatch";
|
|
8
|
+
import * as z from "zod";
|
|
9
|
+
import { access, glob, mkdir, readFile, rm, watch, writeFile } from "node:fs/promises";
|
|
10
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
11
|
+
import { collectMessages, generateHash } from "@saykit/babel-plugin/core";
|
|
12
|
+
|
|
13
|
+
//#region src/shapes.ts
|
|
14
|
+
const Message = z.object({
|
|
15
|
+
message: z.string(),
|
|
16
|
+
translation: z.string().optional(),
|
|
17
|
+
id: z.string().optional(),
|
|
18
|
+
context: z.string().optional(),
|
|
19
|
+
comments: z.string().array(),
|
|
20
|
+
references: z.string().array()
|
|
21
|
+
});
|
|
22
|
+
const Formatter = z.object({
|
|
23
|
+
extension: z.templateLiteral([".", z.string()]).transform((v) => v.slice(1)),
|
|
24
|
+
parse: z.custom((v) => typeof v === "function"),
|
|
25
|
+
stringify: z.custom((v) => typeof v === "function")
|
|
26
|
+
});
|
|
27
|
+
async function tryImport(id) {
|
|
28
|
+
const require = createRequire(join(process.cwd(), "noop.js"));
|
|
29
|
+
try {
|
|
30
|
+
return ok(await import(pathToFileURL(require.resolve(id)).toString()));
|
|
31
|
+
} catch {
|
|
32
|
+
return err(`Cannot find package '${id}', required by saykit`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
const Bucket = z.object({
|
|
36
|
+
include: z.array(z.string()),
|
|
37
|
+
exclude: z.array(z.string()).optional(),
|
|
38
|
+
output: z.templateLiteral([
|
|
39
|
+
z.string(),
|
|
40
|
+
"{locale}",
|
|
41
|
+
z.string(),
|
|
42
|
+
".{extension}"
|
|
43
|
+
]),
|
|
44
|
+
formatter: Formatter.optional().transform(async (formatter, context) => {
|
|
45
|
+
if (formatter) return formatter;
|
|
46
|
+
const module = await tryImport("@saykit/format-po");
|
|
47
|
+
if (module.isErr()) {
|
|
48
|
+
context.addIssue(module.error);
|
|
49
|
+
return z.NEVER;
|
|
50
|
+
}
|
|
51
|
+
formatter = module.value.default();
|
|
52
|
+
const result = Formatter.safeParse(formatter);
|
|
53
|
+
if (result.error) {
|
|
54
|
+
for (const issue of result.error.issues) context.addIssue({ ...issue });
|
|
55
|
+
return z.NEVER;
|
|
56
|
+
}
|
|
57
|
+
return result.data;
|
|
58
|
+
})
|
|
59
|
+
}).transform((v) => ({
|
|
60
|
+
...v,
|
|
61
|
+
match: picomatch(v.include, { ignore: v.exclude })
|
|
62
|
+
}));
|
|
63
|
+
const Configuration = z.object({
|
|
64
|
+
sourceLocale: z.string(),
|
|
65
|
+
locales: z.tuple([z.string()], z.string()),
|
|
66
|
+
fallbackLocales: z.record(z.string(), z.array(z.string())).optional(),
|
|
67
|
+
buckets: z.array(Bucket)
|
|
68
|
+
}).refine((c) => c.sourceLocale === c.locales[0], "sourceLocale must be the same as the first locale");
|
|
69
|
+
|
|
70
|
+
//#endregion
|
|
71
|
+
//#region src/features/loader/explorer.ts
|
|
72
|
+
function getFilesToTry(name) {
|
|
73
|
+
return [
|
|
74
|
+
`.${name}rc`,
|
|
75
|
+
`.${name}rc.json`,
|
|
76
|
+
`.${name}rc.yaml`,
|
|
77
|
+
`.${name}rc.yml`,
|
|
78
|
+
`.${name}rc.js`,
|
|
79
|
+
`.${name}rc.cjs`,
|
|
80
|
+
`.${name}rc.mjs`,
|
|
81
|
+
`.${name}rc.ts`,
|
|
82
|
+
`.${name}rc.mts`,
|
|
83
|
+
`.${name}rc.cts`,
|
|
84
|
+
`${name}.config.json`,
|
|
85
|
+
`${name}.config.js`,
|
|
86
|
+
`${name}.config.cjs`,
|
|
87
|
+
`${name}.config.mjs`,
|
|
88
|
+
`${name}.config.ts`,
|
|
89
|
+
`${name}.config.mts`,
|
|
90
|
+
`${name}.config.cts`,
|
|
91
|
+
"package.json"
|
|
92
|
+
];
|
|
93
|
+
}
|
|
94
|
+
async function findConfigFile(moduleName, projectDir) {
|
|
95
|
+
for (const fileName of getFilesToTry(moduleName)) try {
|
|
96
|
+
const id = join(projectDir, fileName);
|
|
97
|
+
await access(id);
|
|
98
|
+
return {
|
|
99
|
+
id,
|
|
100
|
+
content: await readFile(id, "utf8")
|
|
101
|
+
};
|
|
102
|
+
} catch {}
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
//#endregion
|
|
107
|
+
//#region src/features/loader/loaders.ts
|
|
108
|
+
const js = async (path, _content) => {
|
|
109
|
+
try {
|
|
110
|
+
const { href } = pathToFileURL(path);
|
|
111
|
+
return (await import(href)).default;
|
|
112
|
+
} catch (importError) {
|
|
113
|
+
try {
|
|
114
|
+
const require = globalThis.require ?? createRequire(path);
|
|
115
|
+
const module = require(path);
|
|
116
|
+
delete require.cache[require.resolve(path)];
|
|
117
|
+
return module?.default ?? module;
|
|
118
|
+
} catch (requireError) {
|
|
119
|
+
throw new Error("Failed to import module", { cause: [importError, requireError] });
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
const json = async (_path, content) => {
|
|
124
|
+
try {
|
|
125
|
+
return JSON.parse(content);
|
|
126
|
+
} catch (error) {
|
|
127
|
+
throw new Error("Failed to parse JSON", { cause: error });
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
let YAML;
|
|
131
|
+
(function(_YAML) {
|
|
132
|
+
function parse(text) {
|
|
133
|
+
const lines = text.split(/\r?\n/).filter((l) => l.trim() !== "").map((l) => [l.match(/^\s*/)[0].length, l]);
|
|
134
|
+
const root = {};
|
|
135
|
+
const stack = [{
|
|
136
|
+
indent: -1,
|
|
137
|
+
container: root
|
|
138
|
+
}];
|
|
139
|
+
function parseValue(value, fallback) {
|
|
140
|
+
if (value === "true") return true;
|
|
141
|
+
if (value === "false") return false;
|
|
142
|
+
if (!Number.isNaN(Number(value)) && value !== "") return Number(value);
|
|
143
|
+
if (value.startsWith("[") && value.endsWith("]")) return value.slice(1, -1).split(",").map((e) => parseValue(e.trim()));
|
|
144
|
+
return value || fallback || value;
|
|
145
|
+
}
|
|
146
|
+
for (let [indent, line] of lines) {
|
|
147
|
+
const parent = stack.at(-1);
|
|
148
|
+
while (stack.length > 1 && indent <= parent.indent) stack.pop();
|
|
149
|
+
if (line.startsWith("- ")) {
|
|
150
|
+
line = line.slice(2).trim();
|
|
151
|
+
let value;
|
|
152
|
+
if (line === "") value = {};
|
|
153
|
+
else if (line.includes(":")) {
|
|
154
|
+
const [k, ...r] = line.split(":");
|
|
155
|
+
value = { [k.trim()]: parseValue(r.join(":").trim()) };
|
|
156
|
+
} else value = parseValue(line);
|
|
157
|
+
if (!Array.isArray(parent.container)) {
|
|
158
|
+
const upper = stack.at(-2);
|
|
159
|
+
if (!Array.isArray(upper.container)) {
|
|
160
|
+
for (const key in upper.container) if (upper.container[key] === parent.container) {
|
|
161
|
+
upper.container[key] = [value];
|
|
162
|
+
stack.push({
|
|
163
|
+
indent,
|
|
164
|
+
container: upper.container[key]
|
|
165
|
+
});
|
|
166
|
+
break;
|
|
167
|
+
}
|
|
168
|
+
} else throw new Error("Invalid YAML: expected parent object for array conversion");
|
|
169
|
+
} else parent.container.push(value);
|
|
170
|
+
if (typeof value === "object" && value && !Array.isArray(value)) stack.push({
|
|
171
|
+
indent,
|
|
172
|
+
container: value
|
|
173
|
+
});
|
|
174
|
+
} else {
|
|
175
|
+
const [key, ...rest] = line.split(":");
|
|
176
|
+
const value = parseValue(rest.join(":").trim(), {});
|
|
177
|
+
if (!Array.isArray(parent.container)) parent.container[key.trim()] = value;
|
|
178
|
+
else throw new Error("Invalid YAML: cannot assign key to non-object");
|
|
179
|
+
if (typeof value === "object" && value && !Array.isArray(value)) stack.push({
|
|
180
|
+
indent,
|
|
181
|
+
container: value
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return root;
|
|
186
|
+
}
|
|
187
|
+
_YAML.parse = parse;
|
|
188
|
+
})(YAML || (YAML = {}));
|
|
189
|
+
const yaml = async (_path, content) => {
|
|
190
|
+
try {
|
|
191
|
+
return YAML.parse(content);
|
|
192
|
+
} catch (error) {
|
|
193
|
+
throw new Error("Failed to parse YAML", { cause: error });
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
const ts = async (path, content) => {
|
|
197
|
+
const ts = await import("typescript");
|
|
198
|
+
const outputPath = `${path}.${Date.now()}.js`;
|
|
199
|
+
try {
|
|
200
|
+
const tsConfigPath = ts.findConfigFile(dirname(path), ts.sys.fileExists) || "tsconfig.json";
|
|
201
|
+
const { config: tsConfig, error } = ts.readConfigFile(tsConfigPath, ts.sys.readFile);
|
|
202
|
+
if (error) throw error;
|
|
203
|
+
tsConfig.compilerOptions = {
|
|
204
|
+
...tsConfig.compilerOptions,
|
|
205
|
+
module: ts.ModuleKind.ES2022,
|
|
206
|
+
moduleResolution: ts.ModuleResolutionKind.Bundler,
|
|
207
|
+
target: ts.ScriptTarget.ES2022,
|
|
208
|
+
noEmit: false
|
|
209
|
+
};
|
|
210
|
+
const transpiledContent = ts.transpileModule(content, tsConfig).outputText;
|
|
211
|
+
await writeFile(outputPath, transpiledContent);
|
|
212
|
+
return await js(outputPath, transpiledContent);
|
|
213
|
+
} catch (error) {
|
|
214
|
+
throw new Error("Failed to import module", { cause: error });
|
|
215
|
+
} finally {
|
|
216
|
+
if (ts.sys.fileExists(outputPath)) await rm(outputPath);
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
const detect = async (path, content) => {
|
|
220
|
+
try {
|
|
221
|
+
return await yaml(path, content);
|
|
222
|
+
} catch {
|
|
223
|
+
try {
|
|
224
|
+
return await json(path, content);
|
|
225
|
+
} catch {
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
var loaders_default = Object.freeze({
|
|
231
|
+
".js": js,
|
|
232
|
+
".mjs": js,
|
|
233
|
+
".cjs": js,
|
|
234
|
+
".json": json,
|
|
235
|
+
".yaml": yaml,
|
|
236
|
+
".yml": yaml,
|
|
237
|
+
"": detect,
|
|
238
|
+
".ts": ts,
|
|
239
|
+
".mts": ts,
|
|
240
|
+
".cts": ts
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
//#endregion
|
|
244
|
+
//#region src/features/loader/resolve.ts
|
|
245
|
+
async function useConfig(name = "saykit") {
|
|
246
|
+
const file = await findConfigFile(name, process.cwd());
|
|
247
|
+
if (!file) throw new Error(`Could not find config file for "${name}"`);
|
|
248
|
+
const ext = extname(file.id).toLowerCase();
|
|
249
|
+
let config = await (ext in loaders_default ? loaders_default[ext] : loaders_default[""])(file.id, file.content);
|
|
250
|
+
if (!config || typeof config !== "object") throw new Error(`Invalid config file for "${name}"`);
|
|
251
|
+
if (config && typeof config === "object" && "saykit" in config) config = config.saykit;
|
|
252
|
+
const result = await Configuration.safeParseAsync(config);
|
|
253
|
+
if (result.error) throw new Error(`Invalid config file for "${name}"`, { cause: result.error });
|
|
254
|
+
return result.data;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
//#endregion
|
|
258
|
+
//#region src/features/logger.ts
|
|
259
|
+
const RESET = "\x1B[0m";
|
|
260
|
+
const DIM = "\x1B[2m";
|
|
261
|
+
const BRIGHT = "\x1B[1m";
|
|
262
|
+
const RED = "\x1B[31m";
|
|
263
|
+
const GREEN = "\x1B[32m";
|
|
264
|
+
const YELLOW = "\x1B[33m";
|
|
265
|
+
const BLUE = "\x1B[34m";
|
|
266
|
+
var Logger = class {
|
|
267
|
+
#quiet;
|
|
268
|
+
#verbose;
|
|
269
|
+
constructor(options = {}) {
|
|
270
|
+
this.#quiet = options.quiet ?? false;
|
|
271
|
+
this.#verbose = options.verbose ?? false;
|
|
272
|
+
}
|
|
273
|
+
log(...args) {
|
|
274
|
+
if (!this.#quiet) console.log(...args);
|
|
275
|
+
}
|
|
276
|
+
info(...args) {
|
|
277
|
+
this.log(`${BLUE}🛈${RESET}`, ...args);
|
|
278
|
+
}
|
|
279
|
+
warn(...args) {
|
|
280
|
+
this.log(`${YELLOW}⚠${RESET}`, ...args);
|
|
281
|
+
}
|
|
282
|
+
error(...args) {
|
|
283
|
+
this.log(`${RED}✖${RESET}`, ...args);
|
|
284
|
+
}
|
|
285
|
+
success(...args) {
|
|
286
|
+
this.log(`${GREEN}✔${RESET}`, ...args);
|
|
287
|
+
}
|
|
288
|
+
header(message) {
|
|
289
|
+
this.log(`${BRIGHT}${message}${RESET}`);
|
|
290
|
+
}
|
|
291
|
+
step(message) {
|
|
292
|
+
if (this.#verbose) this.log(` ${DIM}→ ${message}${RESET}`);
|
|
293
|
+
}
|
|
294
|
+
};
|
|
295
|
+
const loggerStorage = new AsyncLocalStorage({ defaultValue: new Logger() });
|
|
296
|
+
|
|
297
|
+
//#endregion
|
|
298
|
+
//#region src/features/extract.ts
|
|
299
|
+
async function extractMessages(path) {
|
|
300
|
+
return collectMessages(path, await readFile(path, "utf8").catch(() => "")).map((m) => ({
|
|
301
|
+
message: m.toICUString(),
|
|
302
|
+
translation: m.toICUString(),
|
|
303
|
+
id: m.descriptor.id,
|
|
304
|
+
context: m.descriptor.context,
|
|
305
|
+
comments: m.comments,
|
|
306
|
+
references: m.references.map((r) => relative(process.cwd(), r).replaceAll("\\", "/"))
|
|
307
|
+
}));
|
|
308
|
+
}
|
|
309
|
+
function mergeUnique(...items) {
|
|
310
|
+
return Array.from(new Set(items.flat()));
|
|
311
|
+
}
|
|
312
|
+
function mergeMessages(messages) {
|
|
313
|
+
const mergedMessages = messages.reduce((map, message) => {
|
|
314
|
+
const key = message.id ?? generateHash(message.message, message.context);
|
|
315
|
+
const existing = map.get(key) ?? message;
|
|
316
|
+
map.set(key, {
|
|
317
|
+
...existing,
|
|
318
|
+
comments: mergeUnique(...existing.comments, ...message.comments),
|
|
319
|
+
references: mergeUnique(...existing.references, ...message.references)
|
|
320
|
+
});
|
|
321
|
+
return map;
|
|
322
|
+
}, /* @__PURE__ */ new Map());
|
|
323
|
+
return Array.from(mergedMessages.values());
|
|
324
|
+
}
|
|
325
|
+
function reconcileMessages(existingMessages, newMessages) {
|
|
326
|
+
const existingMessagesMap = existingMessages.reduce((map, message) => {
|
|
327
|
+
const key = message.id ?? generateHash(message.message, message.context);
|
|
328
|
+
map.set(key, message);
|
|
329
|
+
return map;
|
|
330
|
+
}, /* @__PURE__ */ new Map());
|
|
331
|
+
const updatedMessagesMap = newMessages.reduce((map, message) => {
|
|
332
|
+
const key = message.id ?? generateHash(message.message, message.context);
|
|
333
|
+
const existingMessage = existingMessagesMap.get(key);
|
|
334
|
+
map.set(key, {
|
|
335
|
+
message: message.message,
|
|
336
|
+
translation: void 0,
|
|
337
|
+
...existingMessage,
|
|
338
|
+
id: message.id,
|
|
339
|
+
context: message.context,
|
|
340
|
+
comments: message.comments,
|
|
341
|
+
references: message.references
|
|
342
|
+
});
|
|
343
|
+
return map;
|
|
344
|
+
}, /* @__PURE__ */ new Map());
|
|
345
|
+
return Array.from(updatedMessagesMap.values());
|
|
346
|
+
}
|
|
347
|
+
function expandOutputPath(bucket, locale, extension = bucket.formatter.extension) {
|
|
348
|
+
return resolve(bucket.output.replaceAll("{locale}", locale).replaceAll("{extension}", extension));
|
|
349
|
+
}
|
|
350
|
+
async function readMessagesFromDisk(bucket, locale, path = expandOutputPath(bucket, locale)) {
|
|
351
|
+
const content = await readFile(path, "utf8").catch(() => "");
|
|
352
|
+
if (!content) return [];
|
|
353
|
+
return await bucket.formatter.parse(content, { locale });
|
|
354
|
+
}
|
|
355
|
+
async function writeMessagesToDisk(bucket, locale, messages, path = expandOutputPath(bucket, locale)) {
|
|
356
|
+
const content = await bucket.formatter.stringify(messages, { locale });
|
|
357
|
+
await mkdir(dirname(path), { recursive: true });
|
|
358
|
+
await writeFile(path, content);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
//#endregion
|
|
362
|
+
//#region src/features/compile.ts
|
|
363
|
+
async function hydrateTranslations(cache, config, bucket, locale, messages) {
|
|
364
|
+
if (cache.has(locale)) return cache.get(locale);
|
|
365
|
+
const translations = await applyFallbackTranslations(cache, config, bucket, locale, messages);
|
|
366
|
+
cache.set(locale, translations);
|
|
367
|
+
return translations;
|
|
368
|
+
}
|
|
369
|
+
function getFallbackChain(config, locale) {
|
|
370
|
+
return [...config.fallbackLocales?.[locale] ?? [], config.sourceLocale];
|
|
371
|
+
}
|
|
372
|
+
async function applyFallbackTranslations(cache, config, bucket, locale, messages) {
|
|
373
|
+
const fallbacks = getFallbackChain(config, locale);
|
|
374
|
+
const translations = {};
|
|
375
|
+
for (const message of messages) {
|
|
376
|
+
const key = message.id ?? generateHash(message.message, message.context);
|
|
377
|
+
if (message.translation) {
|
|
378
|
+
translations[key] = message.translation;
|
|
379
|
+
continue;
|
|
380
|
+
}
|
|
381
|
+
for (const fallback of fallbacks) {
|
|
382
|
+
const fallbackMessages = await hydrateTranslations(cache, config, bucket, fallback, messages);
|
|
383
|
+
if (fallbackMessages[key]) {
|
|
384
|
+
translations[key] = fallbackMessages[key];
|
|
385
|
+
break;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
return translations;
|
|
390
|
+
}
|
|
391
|
+
async function writeTranslationsToDisk(bucket, locale, translations, path = expandOutputPath(bucket, locale, "json")) {
|
|
392
|
+
const content = JSON.stringify(translations, null, 2);
|
|
393
|
+
await mkdir(dirname(path), { recursive: true });
|
|
394
|
+
await writeFile(path, content);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
//#endregion
|
|
398
|
+
//#region src/features/watch.ts
|
|
399
|
+
/**
|
|
400
|
+
* Expand a buckets include and exclude patterns into a flat list of file paths.
|
|
401
|
+
*/
|
|
402
|
+
async function globBucket(bucket) {
|
|
403
|
+
const paths = [];
|
|
404
|
+
for await (const file of glob(bucket.include, {
|
|
405
|
+
exclude: bucket.exclude,
|
|
406
|
+
withFileTypes: true
|
|
407
|
+
})) if (file.isFile()) paths.push(join(file.parentPath, file.name));
|
|
408
|
+
return paths;
|
|
409
|
+
}
|
|
410
|
+
/**
|
|
411
|
+
* Watches a path for changes, emitting a debounced event every set delay.
|
|
412
|
+
*
|
|
413
|
+
* Unlike Node's native `fs.watch` method, this:
|
|
414
|
+
* - coalesces rapid consecutive events per file
|
|
415
|
+
* - emits only the final event after `delay` ms of inactivity
|
|
416
|
+
* - deduplicates events by filename
|
|
417
|
+
*/
|
|
418
|
+
async function* watchDebounced(path, options, delay = 300) {
|
|
419
|
+
const timers = /* @__PURE__ */ new Map();
|
|
420
|
+
const queue = /* @__PURE__ */ new Map();
|
|
421
|
+
const resolvers = /* @__PURE__ */ new Map();
|
|
422
|
+
(async () => {
|
|
423
|
+
for await (const event of watch(path, options)) {
|
|
424
|
+
const key = event.filename ?? "__unknown__";
|
|
425
|
+
if (timers.has(key)) clearTimeout(timers.get(key));
|
|
426
|
+
if (!queue.has(key)) queue.set(key, new Promise((r) => resolvers.set(key, r)));
|
|
427
|
+
timers.set(key, setTimeout(() => {
|
|
428
|
+
resolvers.get(key)?.(event);
|
|
429
|
+
timers.delete(key);
|
|
430
|
+
resolvers.delete(key);
|
|
431
|
+
}, delay));
|
|
432
|
+
}
|
|
433
|
+
})();
|
|
434
|
+
while (true) if (queue.size) {
|
|
435
|
+
const next = await Promise.race(queue.values());
|
|
436
|
+
queue.delete(next.filename ?? "__unknown__");
|
|
437
|
+
yield next;
|
|
438
|
+
} else await new Promise((r) => setTimeout(r, 10));
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
//#endregion
|
|
442
|
+
//#region src/features/worker.ts
|
|
443
|
+
function normalisePath(path) {
|
|
444
|
+
return relative(process.cwd(), path).replaceAll("\\", "/");
|
|
445
|
+
}
|
|
446
|
+
var BucketWorker = class {
|
|
447
|
+
config;
|
|
448
|
+
bucket;
|
|
449
|
+
logger;
|
|
450
|
+
constructor(config, bucket, logger) {
|
|
451
|
+
this.config = config;
|
|
452
|
+
this.bucket = bucket;
|
|
453
|
+
this.logger = logger;
|
|
454
|
+
}
|
|
455
|
+
};
|
|
456
|
+
var BucketExtractWorker = class extends BucketWorker {
|
|
457
|
+
index = /* @__PURE__ */ new Map();
|
|
458
|
+
get messages() {
|
|
459
|
+
return Array.from(this.index.values()).flat();
|
|
460
|
+
}
|
|
461
|
+
async indexPath(path) {
|
|
462
|
+
const rp = normalisePath(path);
|
|
463
|
+
this.logger.step(`Processing ${rp}`);
|
|
464
|
+
const messages = await extractMessages(path);
|
|
465
|
+
if (!messages.length) return false;
|
|
466
|
+
this.index.set(path, messages);
|
|
467
|
+
this.logger.step(`Found ${messages.length} messages(s) in ${rp}`);
|
|
468
|
+
return true;
|
|
469
|
+
}
|
|
470
|
+
async scanAll() {
|
|
471
|
+
this.logger.info(`Scanning bucket: ${this.bucket.include}`);
|
|
472
|
+
const paths = await globBucket(this.bucket);
|
|
473
|
+
this.logger.step(`Found ${paths.length} file(s)`);
|
|
474
|
+
await Promise.all(paths.map((p) => this.indexPath(p)));
|
|
475
|
+
this.logger.info(`Total extracted messages: ${this.messages.length}`);
|
|
476
|
+
}
|
|
477
|
+
async writeAll() {
|
|
478
|
+
const newMessages = mergeMessages(this.messages);
|
|
479
|
+
this.logger.info(`Writing ${newMessages.length} messages to locales`);
|
|
480
|
+
for (const locale of this.config.locales) {
|
|
481
|
+
this.logger.step(`Writing locale file for ${locale} to disk`);
|
|
482
|
+
const existingMessages = await readMessagesFromDisk(this.bucket, locale);
|
|
483
|
+
const updatedMessages = locale === this.config.sourceLocale ? newMessages : reconcileMessages(existingMessages, newMessages);
|
|
484
|
+
await writeMessagesToDisk(this.bucket, locale, updatedMessages);
|
|
485
|
+
}
|
|
486
|
+
this.logger.success(`Extraction complete for bucket: ${this.bucket.include}`);
|
|
487
|
+
}
|
|
488
|
+
async updatePath(path) {
|
|
489
|
+
const changed = await this.indexPath(path);
|
|
490
|
+
if (changed) await this.writeAll();
|
|
491
|
+
return changed;
|
|
492
|
+
}
|
|
493
|
+
};
|
|
494
|
+
var BucketCompileWorker = class extends BucketWorker {
|
|
495
|
+
cache = /* @__PURE__ */ new Map();
|
|
496
|
+
async compileAll() {
|
|
497
|
+
this.logger.info(`Compiling bucket: ${this.bucket.include}`);
|
|
498
|
+
for (const locale of this.config.locales) await this.compileLocale(locale);
|
|
499
|
+
this.logger.success(`Compilation complete for bucket: ${this.bucket.include}`);
|
|
500
|
+
}
|
|
501
|
+
async compileLocale(locale) {
|
|
502
|
+
this.logger.step(`Compiling locale: ${locale}`);
|
|
503
|
+
const messages = await readMessagesFromDisk(this.bucket, locale);
|
|
504
|
+
const translations = await hydrateTranslations(this.cache, this.config, this.bucket, locale, messages);
|
|
505
|
+
this.logger.step(`Writing runtime file for ${locale}`);
|
|
506
|
+
await writeTranslationsToDisk(this.bucket, locale, translations);
|
|
507
|
+
}
|
|
508
|
+
};
|
|
509
|
+
var BucketBuildWorker = class extends BucketWorker {
|
|
510
|
+
extract;
|
|
511
|
+
compile;
|
|
512
|
+
constructor(config, bucket, logger) {
|
|
513
|
+
super(config, bucket, logger);
|
|
514
|
+
this.extract = new BucketExtractWorker(config, bucket, logger);
|
|
515
|
+
this.compile = new BucketCompileWorker(config, bucket, logger);
|
|
516
|
+
}
|
|
517
|
+
async buildAll() {
|
|
518
|
+
await this.extract.scanAll();
|
|
519
|
+
await this.extract.writeAll();
|
|
520
|
+
await this.compile.compileAll();
|
|
521
|
+
}
|
|
522
|
+
async watch() {
|
|
523
|
+
this.logger.header(`👀 Watching bucket for changes: ${this.bucket.include}`);
|
|
524
|
+
for await (const event of watchDebounced(".", { recursive: true })) {
|
|
525
|
+
if (!event.filename || !this.bucket.match(event.filename)) continue;
|
|
526
|
+
const filePath = join(process.cwd(), event.filename);
|
|
527
|
+
if (await this.extract.updatePath(filePath)) {
|
|
528
|
+
this.logger.info(`Recompiling due to changes in ${normalisePath(filePath)}`);
|
|
529
|
+
await this.compile.compileAll();
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
};
|
|
534
|
+
|
|
535
|
+
//#endregion
|
|
536
|
+
//#region src/commands/build.ts
|
|
537
|
+
var build_default = new Command().name("build").description("").option("-v, --verbose", "enable verbose logging", false).option("-q, --quiet", "suppress all logging", false).action(async (options) => {
|
|
538
|
+
const config = await useConfig("saykit");
|
|
539
|
+
const logger = new Logger(options);
|
|
540
|
+
logger.header("🏗 Building Messages");
|
|
541
|
+
const tasks = config.buckets.map(async (bucket) => {
|
|
542
|
+
await new BucketBuildWorker(config, bucket, logger).buildAll();
|
|
543
|
+
});
|
|
544
|
+
await Promise.allSettled(tasks);
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
//#endregion
|
|
548
|
+
//#region src/commands/compile.ts
|
|
549
|
+
var compile_default = new Command("compile").description("Compile translations into runtime-ready locale files").option("-v, --verbose", "enable verbose logging", false).option("-q, --quiet", "suppress all logging", false).action(async (options) => {
|
|
550
|
+
const config = await useConfig("saykit");
|
|
551
|
+
const logger = new Logger(options);
|
|
552
|
+
logger.header("🛠 Compiling Translations");
|
|
553
|
+
const tasks = config.buckets.map(async (bucket) => {
|
|
554
|
+
await new BucketCompileWorker(config, bucket, logger).compileAll();
|
|
555
|
+
});
|
|
556
|
+
await Promise.allSettled(tasks);
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
//#endregion
|
|
560
|
+
//#region src/commands/extract.ts
|
|
561
|
+
var extract_default = new Command("extract").description("Extract messages from source files").option("-v, --verbose", "enable verbose logging", false).option("-q, --quiet", "suppress all logging", false).action(async (options) => {
|
|
562
|
+
const config = await useConfig("saykit");
|
|
563
|
+
const logger = new Logger(options);
|
|
564
|
+
logger.header("🛠 Extracting Messages");
|
|
565
|
+
const tasks = config.buckets.map(async (bucket) => {
|
|
566
|
+
const worker = new BucketExtractWorker(config, bucket, logger);
|
|
567
|
+
await worker.scanAll();
|
|
568
|
+
await worker.writeAll();
|
|
569
|
+
});
|
|
570
|
+
await Promise.allSettled(tasks);
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
//#endregion
|
|
574
|
+
//#region src/commands/index.ts
|
|
575
|
+
program.name("saykit").helpOption("-h, --help", "Display help for command").helpCommand("help [command]", "Display help for command").addCommand(extract_default).addCommand(compile_default).addCommand(build_default).parse();
|
|
576
|
+
|
|
577
|
+
//#endregion
|
|
578
|
+
export { };
|
|
579
|
+
//# sourceMappingURL=index.mjs.map
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import picomatch from "picomatch";
|
|
2
|
+
import * as z from "zod";
|
|
3
|
+
import { input } from "zod";
|
|
4
|
+
|
|
5
|
+
//#region src/shapes.d.ts
|
|
6
|
+
declare const Message: z.ZodObject<{
|
|
7
|
+
message: z.ZodString;
|
|
8
|
+
translation: z.ZodOptional<z.ZodString>;
|
|
9
|
+
id: z.ZodOptional<z.ZodString>;
|
|
10
|
+
context: z.ZodOptional<z.ZodString>;
|
|
11
|
+
comments: z.ZodArray<z.ZodString>;
|
|
12
|
+
references: z.ZodArray<z.ZodString>;
|
|
13
|
+
}, z.core.$strip>;
|
|
14
|
+
type Message = z.infer<typeof Message>;
|
|
15
|
+
declare const Formatter: z.ZodObject<{
|
|
16
|
+
extension: z.ZodPipe<z.ZodTemplateLiteral<`.${string}`>, z.ZodTransform<string, `.${string}`>>;
|
|
17
|
+
parse: z.ZodCustom<(content: string, context: {
|
|
18
|
+
locale: string;
|
|
19
|
+
}) => Promise<Message[]>, (content: string, context: {
|
|
20
|
+
locale: string;
|
|
21
|
+
}) => Promise<Message[]>>;
|
|
22
|
+
stringify: z.ZodCustom<(messages: Message[], context: {
|
|
23
|
+
locale: string;
|
|
24
|
+
}) => Promise<string>, (messages: Message[], context: {
|
|
25
|
+
locale: string;
|
|
26
|
+
}) => Promise<string>>;
|
|
27
|
+
}, z.core.$strip>;
|
|
28
|
+
type Formatter = z.infer<typeof Formatter>;
|
|
29
|
+
declare const Bucket: z.ZodPipe<z.ZodObject<{
|
|
30
|
+
include: z.ZodArray<z.ZodString>;
|
|
31
|
+
exclude: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
32
|
+
output: z.ZodTemplateLiteral<`${string}{locale}${string}.{extension}`>;
|
|
33
|
+
formatter: z.ZodPipe<z.ZodOptional<z.ZodObject<{
|
|
34
|
+
extension: z.ZodPipe<z.ZodTemplateLiteral<`.${string}`>, z.ZodTransform<string, `.${string}`>>;
|
|
35
|
+
parse: z.ZodCustom<(content: string, context: {
|
|
36
|
+
locale: string;
|
|
37
|
+
}) => Promise<Message[]>, (content: string, context: {
|
|
38
|
+
locale: string;
|
|
39
|
+
}) => Promise<Message[]>>;
|
|
40
|
+
stringify: z.ZodCustom<(messages: Message[], context: {
|
|
41
|
+
locale: string;
|
|
42
|
+
}) => Promise<string>, (messages: Message[], context: {
|
|
43
|
+
locale: string;
|
|
44
|
+
}) => Promise<string>>;
|
|
45
|
+
}, z.core.$strip>>, z.ZodTransform<{
|
|
46
|
+
extension: string;
|
|
47
|
+
parse: (content: string, context: {
|
|
48
|
+
locale: string;
|
|
49
|
+
}) => Promise<Message[]>;
|
|
50
|
+
stringify: (messages: Message[], context: {
|
|
51
|
+
locale: string;
|
|
52
|
+
}) => Promise<string>;
|
|
53
|
+
}, {
|
|
54
|
+
extension: string;
|
|
55
|
+
parse: (content: string, context: {
|
|
56
|
+
locale: string;
|
|
57
|
+
}) => Promise<Message[]>;
|
|
58
|
+
stringify: (messages: Message[], context: {
|
|
59
|
+
locale: string;
|
|
60
|
+
}) => Promise<string>;
|
|
61
|
+
} | undefined>>;
|
|
62
|
+
}, z.core.$strip>, z.ZodTransform<{
|
|
63
|
+
match: picomatch.Matcher;
|
|
64
|
+
include: string[];
|
|
65
|
+
output: `${string}{locale}${string}.{extension}`;
|
|
66
|
+
formatter: {
|
|
67
|
+
extension: string;
|
|
68
|
+
parse: (content: string, context: {
|
|
69
|
+
locale: string;
|
|
70
|
+
}) => Promise<Message[]>;
|
|
71
|
+
stringify: (messages: Message[], context: {
|
|
72
|
+
locale: string;
|
|
73
|
+
}) => Promise<string>;
|
|
74
|
+
};
|
|
75
|
+
exclude?: string[] | undefined;
|
|
76
|
+
}, {
|
|
77
|
+
include: string[];
|
|
78
|
+
output: `${string}{locale}${string}.{extension}`;
|
|
79
|
+
formatter: {
|
|
80
|
+
extension: string;
|
|
81
|
+
parse: (content: string, context: {
|
|
82
|
+
locale: string;
|
|
83
|
+
}) => Promise<Message[]>;
|
|
84
|
+
stringify: (messages: Message[], context: {
|
|
85
|
+
locale: string;
|
|
86
|
+
}) => Promise<string>;
|
|
87
|
+
};
|
|
88
|
+
exclude?: string[] | undefined;
|
|
89
|
+
}>>;
|
|
90
|
+
type Bucket = z.infer<typeof Bucket>;
|
|
91
|
+
declare const Configuration: z.ZodObject<{
|
|
92
|
+
sourceLocale: z.ZodString;
|
|
93
|
+
locales: z.ZodTuple<[z.ZodString], z.ZodString>;
|
|
94
|
+
fallbackLocales: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodArray<z.ZodString>>>;
|
|
95
|
+
buckets: z.ZodArray<z.ZodPipe<z.ZodObject<{
|
|
96
|
+
include: z.ZodArray<z.ZodString>;
|
|
97
|
+
exclude: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
98
|
+
output: z.ZodTemplateLiteral<`${string}{locale}${string}.{extension}`>;
|
|
99
|
+
formatter: z.ZodPipe<z.ZodOptional<z.ZodObject<{
|
|
100
|
+
extension: z.ZodPipe<z.ZodTemplateLiteral<`.${string}`>, z.ZodTransform<string, `.${string}`>>;
|
|
101
|
+
parse: z.ZodCustom<(content: string, context: {
|
|
102
|
+
locale: string;
|
|
103
|
+
}) => Promise<Message[]>, (content: string, context: {
|
|
104
|
+
locale: string;
|
|
105
|
+
}) => Promise<Message[]>>;
|
|
106
|
+
stringify: z.ZodCustom<(messages: Message[], context: {
|
|
107
|
+
locale: string;
|
|
108
|
+
}) => Promise<string>, (messages: Message[], context: {
|
|
109
|
+
locale: string;
|
|
110
|
+
}) => Promise<string>>;
|
|
111
|
+
}, z.core.$strip>>, z.ZodTransform<{
|
|
112
|
+
extension: string;
|
|
113
|
+
parse: (content: string, context: {
|
|
114
|
+
locale: string;
|
|
115
|
+
}) => Promise<Message[]>;
|
|
116
|
+
stringify: (messages: Message[], context: {
|
|
117
|
+
locale: string;
|
|
118
|
+
}) => Promise<string>;
|
|
119
|
+
}, {
|
|
120
|
+
extension: string;
|
|
121
|
+
parse: (content: string, context: {
|
|
122
|
+
locale: string;
|
|
123
|
+
}) => Promise<Message[]>;
|
|
124
|
+
stringify: (messages: Message[], context: {
|
|
125
|
+
locale: string;
|
|
126
|
+
}) => Promise<string>;
|
|
127
|
+
} | undefined>>;
|
|
128
|
+
}, z.core.$strip>, z.ZodTransform<{
|
|
129
|
+
match: picomatch.Matcher;
|
|
130
|
+
include: string[];
|
|
131
|
+
output: `${string}{locale}${string}.{extension}`;
|
|
132
|
+
formatter: {
|
|
133
|
+
extension: string;
|
|
134
|
+
parse: (content: string, context: {
|
|
135
|
+
locale: string;
|
|
136
|
+
}) => Promise<Message[]>;
|
|
137
|
+
stringify: (messages: Message[], context: {
|
|
138
|
+
locale: string;
|
|
139
|
+
}) => Promise<string>;
|
|
140
|
+
};
|
|
141
|
+
exclude?: string[] | undefined;
|
|
142
|
+
}, {
|
|
143
|
+
include: string[];
|
|
144
|
+
output: `${string}{locale}${string}.{extension}`;
|
|
145
|
+
formatter: {
|
|
146
|
+
extension: string;
|
|
147
|
+
parse: (content: string, context: {
|
|
148
|
+
locale: string;
|
|
149
|
+
}) => Promise<Message[]>;
|
|
150
|
+
stringify: (messages: Message[], context: {
|
|
151
|
+
locale: string;
|
|
152
|
+
}) => Promise<string>;
|
|
153
|
+
};
|
|
154
|
+
exclude?: string[] | undefined;
|
|
155
|
+
}>>>;
|
|
156
|
+
}, z.core.$strip>;
|
|
157
|
+
type Configuration = z.infer<typeof Configuration>;
|
|
158
|
+
//#endregion
|
|
159
|
+
//#region src/define.d.ts
|
|
160
|
+
declare function defineConfig<C extends input<typeof Configuration>>(config: C): C;
|
|
161
|
+
//#endregion
|
|
162
|
+
export { Bucket, Configuration, Formatter, Message, defineConfig };
|
|
163
|
+
//# sourceMappingURL=index.d.mts.map
|
package/dist/index.mjs
ADDED
package/dist/schema.json
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"type": "object",
|
|
4
|
+
"properties": {
|
|
5
|
+
"sourceLocale": {
|
|
6
|
+
"type": "string"
|
|
7
|
+
},
|
|
8
|
+
"locales": {
|
|
9
|
+
"type": "array",
|
|
10
|
+
"items": [
|
|
11
|
+
{
|
|
12
|
+
"type": "string"
|
|
13
|
+
}
|
|
14
|
+
],
|
|
15
|
+
"additionalItems": {
|
|
16
|
+
"type": "string"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"fallbackLocales": {
|
|
20
|
+
"type": "object",
|
|
21
|
+
"propertyNames": {
|
|
22
|
+
"type": "string"
|
|
23
|
+
},
|
|
24
|
+
"additionalProperties": {
|
|
25
|
+
"type": "array",
|
|
26
|
+
"items": {
|
|
27
|
+
"type": "string"
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
"buckets": {
|
|
32
|
+
"type": "array",
|
|
33
|
+
"items": {
|
|
34
|
+
"type": "object",
|
|
35
|
+
"properties": {
|
|
36
|
+
"include": {
|
|
37
|
+
"type": "array",
|
|
38
|
+
"items": {
|
|
39
|
+
"type": "string"
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
"exclude": {
|
|
43
|
+
"type": "array",
|
|
44
|
+
"items": {
|
|
45
|
+
"type": "string"
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
"output": {
|
|
49
|
+
"type": "string",
|
|
50
|
+
"pattern": "^[\\s\\S]{0,}\\{locale\\}[\\s\\S]{0,}\\.\\{extension\\}$"
|
|
51
|
+
},
|
|
52
|
+
"formatter": {
|
|
53
|
+
"type": "null",
|
|
54
|
+
"properties": {
|
|
55
|
+
"extension": {
|
|
56
|
+
"type": "null",
|
|
57
|
+
"pattern": "^\\.[\\s\\S]{0,}$"
|
|
58
|
+
},
|
|
59
|
+
"parse": {
|
|
60
|
+
"type": "null"
|
|
61
|
+
},
|
|
62
|
+
"stringify": {
|
|
63
|
+
"type": "null"
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
"required": [
|
|
67
|
+
"extension",
|
|
68
|
+
"parse",
|
|
69
|
+
"stringify"
|
|
70
|
+
]
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
"required": [
|
|
74
|
+
"include",
|
|
75
|
+
"output"
|
|
76
|
+
]
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
"required": [
|
|
81
|
+
"sourceLocale",
|
|
82
|
+
"locales",
|
|
83
|
+
"buckets"
|
|
84
|
+
]
|
|
85
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@saykit/config",
|
|
3
|
+
"version": "0.0.0-beta-20260309151609",
|
|
4
|
+
"description": "CLI and configuration tooling for saykit",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"cli",
|
|
7
|
+
"config",
|
|
8
|
+
"i18n",
|
|
9
|
+
"saykit"
|
|
10
|
+
],
|
|
11
|
+
"homepage": "https://github.com/k0d13/saykit#readme",
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/k0d13/saykit/issues"
|
|
14
|
+
},
|
|
15
|
+
"license": "MIT",
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "git+https://github.com/k0d13/saykit.git",
|
|
19
|
+
"directory": "packages/config"
|
|
20
|
+
},
|
|
21
|
+
"bin": {
|
|
22
|
+
"saykit": "./dist/commands/index.mjs"
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
"dist",
|
|
26
|
+
"!dist/**/*.map"
|
|
27
|
+
],
|
|
28
|
+
"type": "module",
|
|
29
|
+
"exports": {
|
|
30
|
+
".": {
|
|
31
|
+
"types": "./dist/index.d.mts",
|
|
32
|
+
"default": "./dist/index.mjs"
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
"publishConfig": {
|
|
36
|
+
"access": "public",
|
|
37
|
+
"provenance": true
|
|
38
|
+
},
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"@commander-js/extra-typings": "^14.0.0",
|
|
41
|
+
"commander": "^14.0.3",
|
|
42
|
+
"neverthrow": "^8.2.0",
|
|
43
|
+
"picomatch": "^4.0.3",
|
|
44
|
+
"zod": "^4.3.6",
|
|
45
|
+
"@saykit/babel-plugin": "0.0.0-beta-20260309151609"
|
|
46
|
+
},
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"@types/picomatch": "^4.0.2",
|
|
49
|
+
"typescript": "^5.9.3"
|
|
50
|
+
},
|
|
51
|
+
"peerDependencies": {
|
|
52
|
+
"typescript": "*"
|
|
53
|
+
},
|
|
54
|
+
"scripts": {
|
|
55
|
+
"check": "tsc --noEmit",
|
|
56
|
+
"build": "tsdown"
|
|
57
|
+
}
|
|
58
|
+
}
|