@saykit/config 0.11.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -4
- package/dist/commands/index.cjs +59 -42
- package/dist/commands/index.mjs +51 -34
- package/dist/compile-BrwRUqtQ.cjs +286 -0
- package/dist/compile-VUAk0xEx.mjs +233 -0
- package/dist/emit-NFgnvdzK.cjs +182 -0
- package/dist/emit-q78c-G_t.mjs +117 -0
- package/dist/features/catalogue/index.cjs +12 -47
- package/dist/features/catalogue/index.d.cts +38 -33
- package/dist/features/catalogue/index.d.mts +38 -33
- package/dist/features/catalogue/index.mjs +1 -39
- package/dist/features/loader/index.cjs +1 -1
- package/dist/features/loader/index.d.cts +3 -6
- package/dist/features/loader/index.d.mts +3 -6
- package/dist/features/loader/index.mjs +1 -2
- package/dist/features/messages/index.cjs +15 -65
- package/dist/features/messages/index.d.cts +109 -54
- package/dist/features/messages/index.d.mts +109 -54
- package/dist/features/messages/index.mjs +6 -60
- package/dist/index.cjs +3 -6
- package/dist/index.d.cts +19 -21
- package/dist/index.d.mts +19 -21
- package/dist/index.mjs +1 -3
- package/dist/{loader-C6XxYhMl.mjs → loader-1PBbw7yb.mjs} +3 -6
- package/dist/{loader-XE7SprlF.cjs → loader-V92j1gfD.cjs} +8 -11
- package/dist/{shapes-CRlXtss0.d.cts → shapes-Dq0aJdsn.d.cts} +50 -52
- package/dist/{shapes-CRlXtss0.d.mts → shapes-Dq0aJdsn.d.mts} +50 -52
- package/package.json +10 -5
- package/dist/hash-51ce8YiG.cjs +0 -16
- package/dist/hash-DvzpieJD.mjs +0 -11
- package/dist/storage-8485YdBh.mjs +0 -59
- package/dist/storage-DSdC-TYq.cjs +0 -94
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
[](https://codecov.io/gh/k0d13/saykit?flags%5B0%5D=config)
|
|
6
6
|
|
|
7
|
-
Provides the `defineConfig` helper, the Zod-validated schema for `saykit.config.ts`, and the `saykit` CLI
|
|
7
|
+
Provides the `defineConfig` helper, the Zod-validated schema for `saykit.config.ts`, and the `saykit` CLI that extracts messages from your source and compiles catalogues into the modules your app imports.
|
|
8
8
|
|
|
9
9
|
## Install
|
|
10
10
|
|
|
@@ -43,6 +43,7 @@ export default defineConfig({
|
|
|
43
43
|
```sh
|
|
44
44
|
saykit extract # extract messages once
|
|
45
45
|
saykit extract --watch # extract and watch for changes
|
|
46
|
+
saykit compile # compile catalogues to {locale}.js modules
|
|
46
47
|
saykit clean # prune dead entries from other locales
|
|
47
48
|
```
|
|
48
49
|
|
|
@@ -50,9 +51,10 @@ Extraction only writes the **source** locale (the first entry in `locales`). Loc
|
|
|
50
51
|
have a file yet are bootstrapped with an empty, header-only catalogue so a TMS can register them;
|
|
51
52
|
existing translation files are left untouched. Translated content is owned by your TMS.
|
|
52
53
|
|
|
53
|
-
|
|
54
|
-
chain
|
|
55
|
-
|
|
54
|
+
`saykit compile` (which `extract` also runs) writes a `{locale}.js` beside each catalogue, filled
|
|
55
|
+
from its fallback chain so an untranslated key resolves to a fallback locale and ultimately the
|
|
56
|
+
source string. Ignore the `.js` modules in git and compile before dev and build; commit the
|
|
57
|
+
`{locale}.d.ts` beside them.
|
|
56
58
|
|
|
57
59
|
`saykit clean` only ever subtracts from non-source locales: it drops entries that no longer exist in
|
|
58
60
|
the source catalogue and entries with an empty translation, and leaves everything else exactly as it
|
package/dist/commands/index.cjs
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
const
|
|
3
|
-
const require_loader = require("../loader-
|
|
2
|
+
const require_emit = require("../emit-NFgnvdzK.cjs");
|
|
3
|
+
const require_loader = require("../loader-V92j1gfD.cjs");
|
|
4
4
|
let _commander_js_extra_typings = require("@commander-js/extra-typings");
|
|
5
5
|
let node_fs_promises = require("node:fs/promises");
|
|
6
6
|
let node_path = require("node:path");
|
|
7
7
|
let node_async_hooks = require("node:async_hooks");
|
|
8
|
-
|
|
8
|
+
// src/features/logger.ts
|
|
9
9
|
const RESET = "\x1B[0m";
|
|
10
10
|
const DIM = "\x1B[2m";
|
|
11
11
|
const BRIGHT = "\x1B[1m";
|
|
@@ -43,28 +43,54 @@ var Logger = class {
|
|
|
43
43
|
}
|
|
44
44
|
};
|
|
45
45
|
new node_async_hooks.AsyncLocalStorage({ defaultValue: new Logger() });
|
|
46
|
-
|
|
47
|
-
//#region src/commands/clean.ts
|
|
46
|
+
// src/commands/clean.ts
|
|
48
47
|
var clean_default = new _commander_js_extra_typings.Command("clean").description("Remove orphaned and untranslated entries from non-source locale files").option("-v, --verbose", "enable verbose logging", false).option("-q, --quiet", "suppress all logging", false).action(async (options) => {
|
|
49
48
|
const config = require_loader.resolveConfig();
|
|
50
49
|
const logger = new Logger(options);
|
|
51
50
|
logger.header("🧹 Cleaning Locales");
|
|
52
51
|
const [sourceLocale, ...otherLocales] = config.locales;
|
|
53
52
|
for (const bucket of config.buckets) {
|
|
54
|
-
const sourceMessages = await
|
|
53
|
+
const sourceMessages = await require_emit.readCatalogueMessages(bucket, sourceLocale);
|
|
55
54
|
logger.info(`Cleaning ${otherLocales.length} locale(s) against ${sourceLocale}`);
|
|
56
55
|
for (const locale of otherLocales) {
|
|
57
56
|
logger.step(`Cleaning ${locale}`);
|
|
58
|
-
const existingMessages = await
|
|
59
|
-
const prunedMessages =
|
|
57
|
+
const existingMessages = await require_emit.readCatalogueMessages(bucket, locale);
|
|
58
|
+
const prunedMessages = require_emit.pruneLocaleMessages(existingMessages, sourceMessages);
|
|
60
59
|
logger.step(`Removed ${existingMessages.length - prunedMessages.length} entries from ${locale}`);
|
|
61
|
-
await
|
|
60
|
+
await require_emit.writeCatalogueMessages(bucket, locale, prunedMessages);
|
|
62
61
|
}
|
|
63
62
|
}
|
|
64
63
|
logger.success("Locales cleaned");
|
|
65
64
|
});
|
|
66
|
-
|
|
67
|
-
|
|
65
|
+
// src/features/workers/shared.ts
|
|
66
|
+
function normalisePathForLogs(path) {
|
|
67
|
+
return (0, node_path.relative)(process.cwd(), path).replaceAll("\\", "/");
|
|
68
|
+
}
|
|
69
|
+
var BucketWorker = class {
|
|
70
|
+
config;
|
|
71
|
+
bucket;
|
|
72
|
+
logger;
|
|
73
|
+
constructor(config, bucket, logger) {
|
|
74
|
+
this.config = config;
|
|
75
|
+
this.bucket = bucket;
|
|
76
|
+
this.logger = logger;
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
// src/commands/compile.ts
|
|
80
|
+
var compile_default = new _commander_js_extra_typings.Command("compile").description("Compile catalogues into locale modules").option("-v, --verbose", "enable verbose logging", false).option("-q, --quiet", "suppress all logging", false).action(async (options) => {
|
|
81
|
+
const config = require_loader.resolveConfig();
|
|
82
|
+
const logger = new Logger(options);
|
|
83
|
+
logger.header("⚙ Compiling Catalogues");
|
|
84
|
+
for (const bucket of config.buckets) {
|
|
85
|
+
logger.info(`Compiling ${config.locales.length} locale(s): ${bucket.include}`);
|
|
86
|
+
for (const locale of config.locales) {
|
|
87
|
+
const { path, count } = await require_emit.emitCatalogueModule(config, bucket, locale);
|
|
88
|
+
logger.step(`Wrote ${count} message(s) to ${normalisePathForLogs(path)}`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
logger.success("Catalogues compiled");
|
|
92
|
+
});
|
|
93
|
+
// src/features/catalogue/extractor.ts
|
|
68
94
|
async function extractMessagesFromFile(path, bucket) {
|
|
69
95
|
const content = await (0, node_fs_promises.readFile)(path, "utf8").catch(() => "");
|
|
70
96
|
if (!content) return [];
|
|
@@ -74,8 +100,7 @@ async function extractMessagesFromFile(path, bucket) {
|
|
|
74
100
|
references: message.references.map((reference) => (0, node_path.relative)(process.cwd(), reference).replaceAll("\\", "/"))
|
|
75
101
|
}));
|
|
76
102
|
}
|
|
77
|
-
|
|
78
|
-
//#region src/features/watch.ts
|
|
103
|
+
// src/features/watch.ts
|
|
79
104
|
async function globBucket(bucket) {
|
|
80
105
|
const paths = [];
|
|
81
106
|
for await (const path of (0, node_fs_promises.glob)(bucket.include, { exclude: bucket.exclude })) try {
|
|
@@ -107,23 +132,7 @@ async function* watchDebounced(path, options, delay = 300) {
|
|
|
107
132
|
yield next;
|
|
108
133
|
} else await new Promise((r) => setTimeout(r, 10));
|
|
109
134
|
}
|
|
110
|
-
|
|
111
|
-
//#region src/features/workers/shared.ts
|
|
112
|
-
function normalisePathForLogs(path) {
|
|
113
|
-
return (0, node_path.relative)(process.cwd(), path).replaceAll("\\", "/");
|
|
114
|
-
}
|
|
115
|
-
var BucketWorker = class {
|
|
116
|
-
config;
|
|
117
|
-
bucket;
|
|
118
|
-
logger;
|
|
119
|
-
constructor(config, bucket, logger) {
|
|
120
|
-
this.config = config;
|
|
121
|
-
this.bucket = bucket;
|
|
122
|
-
this.logger = logger;
|
|
123
|
-
}
|
|
124
|
-
};
|
|
125
|
-
//#endregion
|
|
126
|
-
//#region src/features/workers/extract-worker.ts
|
|
135
|
+
// src/features/workers/extract-worker.ts
|
|
127
136
|
function exists(path) {
|
|
128
137
|
return (0, node_fs_promises.access)(path).then(() => true, () => false);
|
|
129
138
|
}
|
|
@@ -158,21 +167,28 @@ var BucketExtractWorker = class extends BucketWorker {
|
|
|
158
167
|
this.logger.info(`Total extracted messages: ${extracted}`);
|
|
159
168
|
}
|
|
160
169
|
async write() {
|
|
161
|
-
const mergedMessages =
|
|
170
|
+
const mergedMessages = require_emit.mergeExtractedMessages(this.#indexedMessages);
|
|
162
171
|
const [sourceLocale, ...otherLocales] = this.config.locales;
|
|
163
172
|
this.logger.info(`Writing ${mergedMessages.length} messages to ${sourceLocale}`);
|
|
164
173
|
this.logger.step(`Writing locale file for ${sourceLocale} to disk`);
|
|
165
|
-
await
|
|
174
|
+
await require_emit.writeCatalogueMessages(this.bucket, sourceLocale, mergedMessages);
|
|
166
175
|
for (const locale of otherLocales) {
|
|
167
|
-
if (await exists(
|
|
176
|
+
if (await exists(require_emit.expandBucketOutputPath(this.bucket, locale))) {
|
|
168
177
|
this.logger.step(`Skipping existing locale file for ${locale}`);
|
|
169
178
|
continue;
|
|
170
179
|
}
|
|
171
180
|
this.logger.step(`Creating empty locale file for ${locale}`);
|
|
172
|
-
await
|
|
181
|
+
await require_emit.writeCatalogueMessages(this.bucket, locale, []);
|
|
173
182
|
}
|
|
183
|
+
await this.compile();
|
|
174
184
|
this.logger.success(`Extraction complete for bucket: ${this.bucket.include}`);
|
|
175
185
|
}
|
|
186
|
+
async compile() {
|
|
187
|
+
for (const locale of this.config.locales) {
|
|
188
|
+
const { path, count } = await require_emit.emitCatalogueModule(this.config, this.bucket, locale);
|
|
189
|
+
this.logger.step(`Compiled ${count} message(s) to ${normalisePathForLogs(path)}`);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
176
192
|
async update(path) {
|
|
177
193
|
const changed = await this.#indexPath(path);
|
|
178
194
|
if (changed) await this.write();
|
|
@@ -181,14 +197,17 @@ var BucketExtractWorker = class extends BucketWorker {
|
|
|
181
197
|
async watch() {
|
|
182
198
|
this.logger.header(`👀 Watching bucket for changes: ${this.bucket.include}`);
|
|
183
199
|
for await (const event of watchDebounced(".", { recursive: true })) {
|
|
184
|
-
if (!event.filename
|
|
200
|
+
if (!event.filename) continue;
|
|
185
201
|
const path = (0, node_path.join)(process.cwd(), event.filename);
|
|
186
|
-
|
|
202
|
+
if (this.bucket.output.match(event.filename)) {
|
|
203
|
+
await this.compile();
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
if (this.bucket.match(event.filename)) await this.update(path);
|
|
187
207
|
}
|
|
188
208
|
}
|
|
189
209
|
};
|
|
190
|
-
|
|
191
|
-
//#region src/commands/extract.ts
|
|
210
|
+
// src/commands/extract.ts
|
|
192
211
|
var extract_default = new _commander_js_extra_typings.Command("extract").description("Extract messages from source files").option("-v, --verbose", "enable verbose logging", false).option("-q, --quiet", "suppress all logging", false).option("-w, --watch", "watch source files for changes", false).action(async (options) => {
|
|
193
212
|
const config = require_loader.resolveConfig();
|
|
194
213
|
const logger = new Logger(options);
|
|
@@ -197,7 +216,5 @@ var extract_default = new _commander_js_extra_typings.Command("extract").descrip
|
|
|
197
216
|
await Promise.all(workers.map((w) => w.scan().then(() => w.write())));
|
|
198
217
|
if (options.watch) await Promise.all(workers.map((w) => w.watch()));
|
|
199
218
|
});
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
_commander_js_extra_typings.program.name("saykit").helpOption("-h, --help", "Display help for command").helpCommand("help [command]", "Display help for command").addCommand(extract_default).addCommand(clean_default).parse();
|
|
203
|
-
//#endregion
|
|
219
|
+
// src/commands/index.ts
|
|
220
|
+
_commander_js_extra_typings.program.name("saykit").helpOption("-h, --help", "Display help for command").helpCommand("help [command]", "Display help for command").addCommand(extract_default).addCommand(compile_default).addCommand(clean_default).parse();
|
package/dist/commands/index.mjs
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
import { t as resolveConfig } from "../loader-
|
|
2
|
+
import { c as writeCatalogueMessages, d as pruneLocaleMessages, l as expandBucketOutputPath, s as readCatalogueMessages, t as emitCatalogueModule, u as mergeExtractedMessages } from "../emit-q78c-G_t.mjs";
|
|
3
|
+
import { t as resolveConfig } from "../loader-1PBbw7yb.mjs";
|
|
4
4
|
import { Command, program } from "@commander-js/extra-typings";
|
|
5
5
|
import { access, glob, readFile, stat, watch } from "node:fs/promises";
|
|
6
6
|
import { join, relative } from "node:path";
|
|
7
7
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
8
|
-
|
|
8
|
+
// src/features/logger.ts
|
|
9
9
|
const RESET = "\x1B[0m";
|
|
10
10
|
const DIM = "\x1B[2m";
|
|
11
11
|
const BRIGHT = "\x1B[1m";
|
|
@@ -43,8 +43,7 @@ var Logger = class {
|
|
|
43
43
|
}
|
|
44
44
|
};
|
|
45
45
|
new AsyncLocalStorage({ defaultValue: new Logger() });
|
|
46
|
-
|
|
47
|
-
//#region src/commands/clean.ts
|
|
46
|
+
// src/commands/clean.ts
|
|
48
47
|
var clean_default = new Command("clean").description("Remove orphaned and untranslated entries from non-source locale files").option("-v, --verbose", "enable verbose logging", false).option("-q, --quiet", "suppress all logging", false).action(async (options) => {
|
|
49
48
|
const config = resolveConfig();
|
|
50
49
|
const logger = new Logger(options);
|
|
@@ -63,8 +62,35 @@ var clean_default = new Command("clean").description("Remove orphaned and untran
|
|
|
63
62
|
}
|
|
64
63
|
logger.success("Locales cleaned");
|
|
65
64
|
});
|
|
66
|
-
|
|
67
|
-
|
|
65
|
+
// src/features/workers/shared.ts
|
|
66
|
+
function normalisePathForLogs(path) {
|
|
67
|
+
return relative(process.cwd(), path).replaceAll("\\", "/");
|
|
68
|
+
}
|
|
69
|
+
var BucketWorker = class {
|
|
70
|
+
config;
|
|
71
|
+
bucket;
|
|
72
|
+
logger;
|
|
73
|
+
constructor(config, bucket, logger) {
|
|
74
|
+
this.config = config;
|
|
75
|
+
this.bucket = bucket;
|
|
76
|
+
this.logger = logger;
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
// src/commands/compile.ts
|
|
80
|
+
var compile_default = new Command("compile").description("Compile catalogues into locale modules").option("-v, --verbose", "enable verbose logging", false).option("-q, --quiet", "suppress all logging", false).action(async (options) => {
|
|
81
|
+
const config = resolveConfig();
|
|
82
|
+
const logger = new Logger(options);
|
|
83
|
+
logger.header("⚙ Compiling Catalogues");
|
|
84
|
+
for (const bucket of config.buckets) {
|
|
85
|
+
logger.info(`Compiling ${config.locales.length} locale(s): ${bucket.include}`);
|
|
86
|
+
for (const locale of config.locales) {
|
|
87
|
+
const { path, count } = await emitCatalogueModule(config, bucket, locale);
|
|
88
|
+
logger.step(`Wrote ${count} message(s) to ${normalisePathForLogs(path)}`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
logger.success("Catalogues compiled");
|
|
92
|
+
});
|
|
93
|
+
// src/features/catalogue/extractor.ts
|
|
68
94
|
async function extractMessagesFromFile(path, bucket) {
|
|
69
95
|
const content = await readFile(path, "utf8").catch(() => "");
|
|
70
96
|
if (!content) return [];
|
|
@@ -74,8 +100,7 @@ async function extractMessagesFromFile(path, bucket) {
|
|
|
74
100
|
references: message.references.map((reference) => relative(process.cwd(), reference).replaceAll("\\", "/"))
|
|
75
101
|
}));
|
|
76
102
|
}
|
|
77
|
-
|
|
78
|
-
//#region src/features/watch.ts
|
|
103
|
+
// src/features/watch.ts
|
|
79
104
|
async function globBucket(bucket) {
|
|
80
105
|
const paths = [];
|
|
81
106
|
for await (const path of glob(bucket.include, { exclude: bucket.exclude })) try {
|
|
@@ -107,23 +132,7 @@ async function* watchDebounced(path, options, delay = 300) {
|
|
|
107
132
|
yield next;
|
|
108
133
|
} else await new Promise((r) => setTimeout(r, 10));
|
|
109
134
|
}
|
|
110
|
-
|
|
111
|
-
//#region src/features/workers/shared.ts
|
|
112
|
-
function normalisePathForLogs(path) {
|
|
113
|
-
return relative(process.cwd(), path).replaceAll("\\", "/");
|
|
114
|
-
}
|
|
115
|
-
var BucketWorker = class {
|
|
116
|
-
config;
|
|
117
|
-
bucket;
|
|
118
|
-
logger;
|
|
119
|
-
constructor(config, bucket, logger) {
|
|
120
|
-
this.config = config;
|
|
121
|
-
this.bucket = bucket;
|
|
122
|
-
this.logger = logger;
|
|
123
|
-
}
|
|
124
|
-
};
|
|
125
|
-
//#endregion
|
|
126
|
-
//#region src/features/workers/extract-worker.ts
|
|
135
|
+
// src/features/workers/extract-worker.ts
|
|
127
136
|
function exists(path) {
|
|
128
137
|
return access(path).then(() => true, () => false);
|
|
129
138
|
}
|
|
@@ -171,8 +180,15 @@ var BucketExtractWorker = class extends BucketWorker {
|
|
|
171
180
|
this.logger.step(`Creating empty locale file for ${locale}`);
|
|
172
181
|
await writeCatalogueMessages(this.bucket, locale, []);
|
|
173
182
|
}
|
|
183
|
+
await this.compile();
|
|
174
184
|
this.logger.success(`Extraction complete for bucket: ${this.bucket.include}`);
|
|
175
185
|
}
|
|
186
|
+
async compile() {
|
|
187
|
+
for (const locale of this.config.locales) {
|
|
188
|
+
const { path, count } = await emitCatalogueModule(this.config, this.bucket, locale);
|
|
189
|
+
this.logger.step(`Compiled ${count} message(s) to ${normalisePathForLogs(path)}`);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
176
192
|
async update(path) {
|
|
177
193
|
const changed = await this.#indexPath(path);
|
|
178
194
|
if (changed) await this.write();
|
|
@@ -181,14 +197,17 @@ var BucketExtractWorker = class extends BucketWorker {
|
|
|
181
197
|
async watch() {
|
|
182
198
|
this.logger.header(`👀 Watching bucket for changes: ${this.bucket.include}`);
|
|
183
199
|
for await (const event of watchDebounced(".", { recursive: true })) {
|
|
184
|
-
if (!event.filename
|
|
200
|
+
if (!event.filename) continue;
|
|
185
201
|
const path = join(process.cwd(), event.filename);
|
|
186
|
-
|
|
202
|
+
if (this.bucket.output.match(event.filename)) {
|
|
203
|
+
await this.compile();
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
if (this.bucket.match(event.filename)) await this.update(path);
|
|
187
207
|
}
|
|
188
208
|
}
|
|
189
209
|
};
|
|
190
|
-
|
|
191
|
-
//#region src/commands/extract.ts
|
|
210
|
+
// src/commands/extract.ts
|
|
192
211
|
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).option("-w, --watch", "watch source files for changes", false).action(async (options) => {
|
|
193
212
|
const config = resolveConfig();
|
|
194
213
|
const logger = new Logger(options);
|
|
@@ -197,8 +216,6 @@ var extract_default = new Command("extract").description("Extract messages from
|
|
|
197
216
|
await Promise.all(workers.map((w) => w.scan().then(() => w.write())));
|
|
198
217
|
if (options.watch) await Promise.all(workers.map((w) => w.watch()));
|
|
199
218
|
});
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
program.name("saykit").helpOption("-h, --help", "Display help for command").helpCommand("help [command]", "Display help for command").addCommand(extract_default).addCommand(clean_default).parse();
|
|
203
|
-
//#endregion
|
|
219
|
+
// src/commands/index.ts
|
|
220
|
+
program.name("saykit").helpOption("-h, --help", "Display help for command").helpCommand("help [command]", "Display help for command").addCommand(extract_default).addCommand(compile_default).addCommand(clean_default).parse();
|
|
204
221
|
export {};
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
let js_sha256 = require("js-sha256");
|
|
2
|
+
let _messageformat_parser = require("@messageformat/parser");
|
|
3
|
+
let _messageformat_date_skeleton = require("@messageformat/date-skeleton");
|
|
4
|
+
let _messageformat_number_skeleton = require("@messageformat/number-skeleton");
|
|
5
|
+
// src/features/messages/hash.ts
|
|
6
|
+
function generateHash(input, context) {
|
|
7
|
+
const hasher = js_sha256.sha256.create();
|
|
8
|
+
hasher.update(`${input}\u{001F}${context || ""}`);
|
|
9
|
+
const elements = hasher.toString().match(/.{1,2}/g)?.map((b) => parseInt(b, 16)) || [];
|
|
10
|
+
const bytes = Uint8Array.from(elements);
|
|
11
|
+
return btoa(String.fromCharCode(...bytes)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "").slice(0, 6);
|
|
12
|
+
}
|
|
13
|
+
// src/features/messages/styles.ts
|
|
14
|
+
const ARGUMENT_STYLES = {
|
|
15
|
+
number: ["integer", "percent"],
|
|
16
|
+
date: [
|
|
17
|
+
"short",
|
|
18
|
+
"medium",
|
|
19
|
+
"long",
|
|
20
|
+
"full"
|
|
21
|
+
],
|
|
22
|
+
time: [
|
|
23
|
+
"short",
|
|
24
|
+
"medium",
|
|
25
|
+
"long",
|
|
26
|
+
"full"
|
|
27
|
+
]
|
|
28
|
+
};
|
|
29
|
+
const ARGUMENT_TYPES = Object.keys(ARGUMENT_STYLES);
|
|
30
|
+
function isArgumentType(kind) {
|
|
31
|
+
return Object.hasOwn(ARGUMENT_STYLES, kind);
|
|
32
|
+
}
|
|
33
|
+
var StyleError = class extends Error {};
|
|
34
|
+
const SKELETON = "::";
|
|
35
|
+
const LITERAL_STYLE_PATTERN = /^[^{}\r\n]*[#0][^{}\r\n]*$/;
|
|
36
|
+
const DATE_STYLES = {
|
|
37
|
+
short: {
|
|
38
|
+
year: "numeric",
|
|
39
|
+
month: "numeric",
|
|
40
|
+
day: "numeric"
|
|
41
|
+
},
|
|
42
|
+
medium: {
|
|
43
|
+
year: "numeric",
|
|
44
|
+
month: "short",
|
|
45
|
+
day: "numeric"
|
|
46
|
+
},
|
|
47
|
+
long: {
|
|
48
|
+
year: "numeric",
|
|
49
|
+
month: "long",
|
|
50
|
+
day: "numeric"
|
|
51
|
+
},
|
|
52
|
+
full: {
|
|
53
|
+
year: "numeric",
|
|
54
|
+
month: "long",
|
|
55
|
+
day: "numeric",
|
|
56
|
+
weekday: "long"
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
const TIME_STYLES = {
|
|
60
|
+
short: {
|
|
61
|
+
hour: "numeric",
|
|
62
|
+
minute: "numeric"
|
|
63
|
+
},
|
|
64
|
+
medium: {
|
|
65
|
+
hour: "numeric",
|
|
66
|
+
minute: "numeric",
|
|
67
|
+
second: "numeric"
|
|
68
|
+
},
|
|
69
|
+
long: {
|
|
70
|
+
hour: "numeric",
|
|
71
|
+
minute: "numeric",
|
|
72
|
+
second: "numeric",
|
|
73
|
+
timeZoneName: "short"
|
|
74
|
+
},
|
|
75
|
+
full: {
|
|
76
|
+
hour: "numeric",
|
|
77
|
+
minute: "numeric",
|
|
78
|
+
second: "numeric",
|
|
79
|
+
timeZoneName: "long"
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
function dateTimeStyle(kind, style) {
|
|
83
|
+
const named = kind === "date" ? DATE_STYLES : TIME_STYLES;
|
|
84
|
+
if (style.startsWith(SKELETON)) {
|
|
85
|
+
const errors = [];
|
|
86
|
+
const tokens = (0, _messageformat_date_skeleton.parseDateTokens)(style.slice(2));
|
|
87
|
+
const opt = (0, _messageformat_date_skeleton.getDateTimeFormatOptions)(tokens, (_type, message) => errors.push(message));
|
|
88
|
+
if (errors.length > 0) throw new StyleError(errors[0]);
|
|
89
|
+
if (Object.keys(opt).length === 0) throw new StyleError("it names no fields");
|
|
90
|
+
return opt;
|
|
91
|
+
}
|
|
92
|
+
if (style === "") return named.medium;
|
|
93
|
+
if (Object.hasOwn(named, style)) return named[style];
|
|
94
|
+
throw new StyleError(`Unsupported ${kind} style ${style}`);
|
|
95
|
+
}
|
|
96
|
+
function numberStyle(style) {
|
|
97
|
+
if (style === "") return {};
|
|
98
|
+
if (style === "integer") return { maximumFractionDigits: 0 };
|
|
99
|
+
if (style === "percent") return { style: "percent" };
|
|
100
|
+
if (style === SKELETON) throw new StyleError("it names no fields");
|
|
101
|
+
const errors = [];
|
|
102
|
+
const onError = (error) => errors.push(String(error));
|
|
103
|
+
const skeleton = style.startsWith(SKELETON) ? (0, _messageformat_number_skeleton.parseNumberSkeleton)(style.slice(2), onError) : (0, _messageformat_number_skeleton.parseNumberPattern)(style, "XXX", onError);
|
|
104
|
+
let scale;
|
|
105
|
+
const opt = (0, _messageformat_number_skeleton.getNumberFormatOptions)(skeleton, (stem, option) => {
|
|
106
|
+
if (stem === "scale") scale = Number(option);
|
|
107
|
+
else errors.push(`unsupported stem '${stem}'`);
|
|
108
|
+
});
|
|
109
|
+
if (errors.length > 0) throw new StyleError(errors[0]);
|
|
110
|
+
return scale === void 0 ? opt : {
|
|
111
|
+
...opt,
|
|
112
|
+
scale
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
function validateArgumentStyle(type, style) {
|
|
116
|
+
const named = ARGUMENT_STYLES[type];
|
|
117
|
+
if (named.includes(style)) return;
|
|
118
|
+
const skeleton = style.startsWith(SKELETON);
|
|
119
|
+
if (skeleton || type === "number" && LITERAL_STYLE_PATTERN.test(style)) try {
|
|
120
|
+
if (type === "number") numberStyle(style);
|
|
121
|
+
else dateTimeStyle(type, style);
|
|
122
|
+
return;
|
|
123
|
+
} catch (error) {
|
|
124
|
+
if (!(error instanceof StyleError)) throw error;
|
|
125
|
+
throw new Error(`Invalid ${type} ${skeleton ? "skeleton" : "pattern"} '${style}': ${error.message}`);
|
|
126
|
+
}
|
|
127
|
+
const expected = named.map((s) => `'${s}'`).join(", ");
|
|
128
|
+
throw new Error(`Invalid ${type} style '${style}', expected ${expected}, or a skeleton such as ` + (type === "number" ? "::currency/EUR" : "::yyyyMMdd") + (type === "number" ? ", or a literal number pattern such as #,##0.00" : ""));
|
|
129
|
+
}
|
|
130
|
+
// src/features/messages/compile.ts
|
|
131
|
+
const Kind = {
|
|
132
|
+
Value: 0,
|
|
133
|
+
Number: 1,
|
|
134
|
+
DateTime: 2,
|
|
135
|
+
Plural: 3,
|
|
136
|
+
Ordinal: 4,
|
|
137
|
+
Select: 5,
|
|
138
|
+
Hash: 6
|
|
139
|
+
};
|
|
140
|
+
const EXACT = /^=\d+$/;
|
|
141
|
+
function key(argument) {
|
|
142
|
+
return `_${argument}`;
|
|
143
|
+
}
|
|
144
|
+
function concat(nodes) {
|
|
145
|
+
if (nodes.length === 0) return "";
|
|
146
|
+
if (nodes.length === 1) return nodes[0];
|
|
147
|
+
return nodes;
|
|
148
|
+
}
|
|
149
|
+
function compileMessage(icu) {
|
|
150
|
+
function argument(token) {
|
|
151
|
+
const value = key(token.arg);
|
|
152
|
+
let style = "";
|
|
153
|
+
for (const part of token.param ?? []) {
|
|
154
|
+
if (part.type !== "content") throw new Error(`Unsupported style part: ${part.type}`);
|
|
155
|
+
style += part.value;
|
|
156
|
+
}
|
|
157
|
+
style = style.trim();
|
|
158
|
+
const resolve = (read, fallback) => {
|
|
159
|
+
try {
|
|
160
|
+
return read(style);
|
|
161
|
+
} catch (error) {
|
|
162
|
+
if (!(error instanceof StyleError)) throw error;
|
|
163
|
+
return fallback();
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
switch (token.key) {
|
|
167
|
+
case "date":
|
|
168
|
+
case "time": {
|
|
169
|
+
const kind = token.key;
|
|
170
|
+
const options = resolve((s) => dateTimeStyle(kind, s), () => dateTimeStyle(kind, ""));
|
|
171
|
+
return [
|
|
172
|
+
Kind.DateTime,
|
|
173
|
+
value,
|
|
174
|
+
options
|
|
175
|
+
];
|
|
176
|
+
}
|
|
177
|
+
case "number": {
|
|
178
|
+
const { scale, ...options } = resolve(numberStyle, () => ({}));
|
|
179
|
+
if (scale !== void 0) return [
|
|
180
|
+
Kind.Number,
|
|
181
|
+
value,
|
|
182
|
+
options,
|
|
183
|
+
scale
|
|
184
|
+
];
|
|
185
|
+
return Object.keys(options).length === 0 ? [Kind.Number, value] : [
|
|
186
|
+
Kind.Number,
|
|
187
|
+
value,
|
|
188
|
+
options
|
|
189
|
+
];
|
|
190
|
+
}
|
|
191
|
+
default: return [Kind.Value, value];
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
function choice(token) {
|
|
195
|
+
const value = key(token.arg);
|
|
196
|
+
const cases = Object.create(null);
|
|
197
|
+
for (const branch of token.cases) {
|
|
198
|
+
const key = EXACT.test(branch.key) ? `=${Number(branch.key.slice(1))}` : branch.key;
|
|
199
|
+
if (!Object.hasOwn(cases, key)) cases[key] = pattern(branch.tokens);
|
|
200
|
+
}
|
|
201
|
+
if (token.type === "select") return [
|
|
202
|
+
Kind.Select,
|
|
203
|
+
value,
|
|
204
|
+
cases
|
|
205
|
+
];
|
|
206
|
+
const kind = token.type === "selectordinal" ? Kind.Ordinal : Kind.Plural;
|
|
207
|
+
const offset = token.pluralOffset ?? 0;
|
|
208
|
+
return offset === 0 ? [
|
|
209
|
+
kind,
|
|
210
|
+
value,
|
|
211
|
+
cases
|
|
212
|
+
] : [
|
|
213
|
+
kind,
|
|
214
|
+
value,
|
|
215
|
+
cases,
|
|
216
|
+
offset
|
|
217
|
+
];
|
|
218
|
+
}
|
|
219
|
+
function fragment(token) {
|
|
220
|
+
switch (token.type) {
|
|
221
|
+
case "content": return token.value;
|
|
222
|
+
case "argument": return [Kind.Value, key(token.arg)];
|
|
223
|
+
case "function": return argument(token);
|
|
224
|
+
case "octothorpe": return [Kind.Hash];
|
|
225
|
+
default: return choice(token);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
function pattern(tokens) {
|
|
229
|
+
return concat(tokens.map(fragment));
|
|
230
|
+
}
|
|
231
|
+
return pattern((0, _messageformat_parser.parse)(icu));
|
|
232
|
+
}
|
|
233
|
+
Object.defineProperty(exports, "ARGUMENT_STYLES", {
|
|
234
|
+
enumerable: true,
|
|
235
|
+
get: function() {
|
|
236
|
+
return ARGUMENT_STYLES;
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
Object.defineProperty(exports, "ARGUMENT_TYPES", {
|
|
240
|
+
enumerable: true,
|
|
241
|
+
get: function() {
|
|
242
|
+
return ARGUMENT_TYPES;
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
Object.defineProperty(exports, "StyleError", {
|
|
246
|
+
enumerable: true,
|
|
247
|
+
get: function() {
|
|
248
|
+
return StyleError;
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
Object.defineProperty(exports, "compileMessage", {
|
|
252
|
+
enumerable: true,
|
|
253
|
+
get: function() {
|
|
254
|
+
return compileMessage;
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
Object.defineProperty(exports, "dateTimeStyle", {
|
|
258
|
+
enumerable: true,
|
|
259
|
+
get: function() {
|
|
260
|
+
return dateTimeStyle;
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
Object.defineProperty(exports, "generateHash", {
|
|
264
|
+
enumerable: true,
|
|
265
|
+
get: function() {
|
|
266
|
+
return generateHash;
|
|
267
|
+
}
|
|
268
|
+
});
|
|
269
|
+
Object.defineProperty(exports, "isArgumentType", {
|
|
270
|
+
enumerable: true,
|
|
271
|
+
get: function() {
|
|
272
|
+
return isArgumentType;
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
Object.defineProperty(exports, "numberStyle", {
|
|
276
|
+
enumerable: true,
|
|
277
|
+
get: function() {
|
|
278
|
+
return numberStyle;
|
|
279
|
+
}
|
|
280
|
+
});
|
|
281
|
+
Object.defineProperty(exports, "validateArgumentStyle", {
|
|
282
|
+
enumerable: true,
|
|
283
|
+
get: function() {
|
|
284
|
+
return validateArgumentStyle;
|
|
285
|
+
}
|
|
286
|
+
});
|