@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.
@@ -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
@@ -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
@@ -0,0 +1,8 @@
1
+ //#region src/define.ts
2
+ function defineConfig(config) {
3
+ return config;
4
+ }
5
+
6
+ //#endregion
7
+ export { defineConfig };
8
+ //# sourceMappingURL=index.mjs.map
@@ -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
+ }