@dialexjs/cli 0.2.3 → 0.5.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 +1 -1
- package/dist/api.d.mts +71 -5
- package/dist/api.d.mts.map +1 -1
- package/dist/api.mjs +2 -2
- package/dist/index.mjs +1084 -856
- package/dist/index.mjs.map +1 -1
- package/dist/{project-BnG6V2I0.mjs → project-BJYmnKiU.mjs} +597 -124
- package/dist/project-BJYmnKiU.mjs.map +1 -0
- package/dist/translate/index.d.mts.map +1 -1
- package/dist/translate/index.mjs.map +1 -1
- package/package.json +2 -2
- package/dist/project-BnG6V2I0.mjs.map +0 -1
|
@@ -1,122 +1,17 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { renderDts, resolveDialexConfig } from "dialexjs/scanner";
|
|
3
4
|
import fg from "fast-glob";
|
|
4
|
-
import chokidar from "chokidar";
|
|
5
|
-
import { generateDts, resolveI18nConfig } from "dialexjs/scanner";
|
|
6
5
|
import pc from "picocolors";
|
|
7
|
-
import { generateCode, parseExpression, parseModule, writeFile } from "magicast";
|
|
8
6
|
import { pathToFileURL } from "node:url";
|
|
9
|
-
import {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
warn: (msg) => console.warn(pc.yellow("⚠ ") + msg),
|
|
15
|
-
error: (msg) => console.error(pc.red("✖ ") + pc.bold(msg)),
|
|
16
|
-
log: (msg) => console.log(msg)
|
|
17
|
-
};
|
|
18
|
-
//#endregion
|
|
19
|
-
//#region src/commands/generate.ts
|
|
20
|
-
function generateDictionaries(root, options = {}) {
|
|
21
|
-
const inlineConfig = options.config ? { configFile: options.config } : {};
|
|
22
|
-
const config = resolveI18nConfig(root, inlineConfig);
|
|
23
|
-
const include = config.include || "**/*.content.ts";
|
|
24
|
-
const files = fg.sync(include, {
|
|
25
|
-
cwd: root,
|
|
26
|
-
absolute: true,
|
|
27
|
-
ignore: [
|
|
28
|
-
"**/node_modules/**",
|
|
29
|
-
"**/dist/**",
|
|
30
|
-
"**/.next/**"
|
|
31
|
-
]
|
|
32
|
-
});
|
|
33
|
-
const srcDir = fs.existsSync(path.join(root, "src")) ? path.join(root, "src") : root;
|
|
34
|
-
const targetOutput = options.output ? path.resolve(root, options.output) : path.join(srcDir, "dialex.generated.ts");
|
|
35
|
-
const targetDir = path.dirname(targetOutput);
|
|
36
|
-
generateDts(root, files, config.locales);
|
|
37
|
-
const dtsPath = path.join(srcDir, "dialex-env.d.ts");
|
|
38
|
-
const importLines = [];
|
|
39
|
-
const dictIdentifiers = [];
|
|
40
|
-
files.forEach((file, idx) => {
|
|
41
|
-
let rel = path.relative(targetDir, file).replace(/\\/g, "/");
|
|
42
|
-
rel = rel.replace(/\.ts$/, ".js");
|
|
43
|
-
if (!rel.startsWith(".")) rel = "./" + rel;
|
|
44
|
-
const id = `dict_${idx}`;
|
|
45
|
-
importLines.push(`import ${id} from "${rel}";`);
|
|
46
|
-
dictIdentifiers.push(id);
|
|
47
|
-
});
|
|
48
|
-
const dictList = dictIdentifiers.length <= 1 ? `[${dictIdentifiers.join(", ")}]` : `[\n ${dictIdentifiers.join(",\n ")}\n]`;
|
|
49
|
-
const content = `// Auto-generated by dialex CLI. Do not edit directly.
|
|
50
|
-
/* eslint-disable */
|
|
51
|
-
// @ts-nocheck
|
|
52
|
-
${importLines.join("\n")}
|
|
53
|
-
|
|
54
|
-
export const dictionaries = ${dictList};
|
|
55
|
-
|
|
56
|
-
export const dictionaryMap = {
|
|
57
|
-
${dictIdentifiers.map((id) => ` [${id}.name]: ${id}.dictionary,`).join("\n")}
|
|
58
|
-
};
|
|
59
|
-
|
|
60
|
-
export default dictionaries;
|
|
61
|
-
`;
|
|
62
|
-
fs.mkdirSync(targetDir, { recursive: true });
|
|
63
|
-
if ((fs.existsSync(targetOutput) ? fs.readFileSync(targetOutput, "utf-8") : "") !== content) fs.writeFileSync(targetOutput, content, "utf-8");
|
|
64
|
-
return {
|
|
65
|
-
files,
|
|
66
|
-
outputPath: targetOutput,
|
|
67
|
-
dtsPath
|
|
68
|
-
};
|
|
69
|
-
}
|
|
70
|
-
async function runGenerate(options = {}) {
|
|
71
|
-
const root = options.cwd || process.cwd();
|
|
72
|
-
const quiet = options.json === true;
|
|
73
|
-
const doGenerate = () => {
|
|
74
|
-
try {
|
|
75
|
-
const { files, outputPath, dtsPath } = generateDictionaries(root, options);
|
|
76
|
-
if (quiet) console.log(JSON.stringify({
|
|
77
|
-
success: true,
|
|
78
|
-
files: files.map((f) => path.relative(root, f)),
|
|
79
|
-
outputPath: path.relative(root, outputPath),
|
|
80
|
-
dtsPath: path.relative(root, dtsPath)
|
|
81
|
-
}, null, 2));
|
|
82
|
-
else {
|
|
83
|
-
logger.success(`Generated dictionaries for ${files.length} file${files.length === 1 ? "" : "s"}:`);
|
|
84
|
-
logger.info(`→ ${path.relative(root, outputPath)}`);
|
|
85
|
-
logger.info(`→ ${path.relative(root, dtsPath)}`);
|
|
86
|
-
}
|
|
87
|
-
return true;
|
|
88
|
-
} catch (err) {
|
|
89
|
-
if (quiet) console.log(JSON.stringify({
|
|
90
|
-
success: false,
|
|
91
|
-
error: err.message || String(err)
|
|
92
|
-
}, null, 2));
|
|
93
|
-
else logger.error(`Generation failed: ${err.message || String(err)}`);
|
|
94
|
-
return false;
|
|
95
|
-
}
|
|
96
|
-
};
|
|
97
|
-
if (!doGenerate() && !options.watch) process.exit(1);
|
|
98
|
-
if (options.watch) {
|
|
99
|
-
logger.info("Watching for dictionary changes (-w, --watch)...");
|
|
100
|
-
const inlineConfig = options.config ? { configFile: options.config } : {};
|
|
101
|
-
const pattern = resolveI18nConfig(root, inlineConfig).include || "**/*.content.ts";
|
|
102
|
-
chokidar.watch(pattern, {
|
|
103
|
-
cwd: root,
|
|
104
|
-
ignoreInitial: true,
|
|
105
|
-
ignored: [
|
|
106
|
-
"**/node_modules/**",
|
|
107
|
-
"**/dist/**",
|
|
108
|
-
"**/.next/**"
|
|
109
|
-
]
|
|
110
|
-
}).on("all", (event, filePath) => {
|
|
111
|
-
logger.info(`[${event}] ${filePath} changed, regenerating...`);
|
|
112
|
-
doGenerate();
|
|
113
|
-
});
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
//#endregion
|
|
7
|
+
import { generateCode, parseExpression, parseModule, writeFile } from "magicast";
|
|
8
|
+
import { argumentSignature, findPlurals, getArguments, isIcuStructured, parseMessage } from "dialexjs/icu";
|
|
9
|
+
import { createHash } from "node:crypto";
|
|
10
|
+
import chokidar from "chokidar";
|
|
11
|
+
import { DEFAULT_CONFIG } from "dialexjs";
|
|
117
12
|
//#region src/utils/dictionary-edit.ts
|
|
118
13
|
const TODO_PREFIX = "[TODO] ";
|
|
119
|
-
const IDENTIFIER = /^[A-Za-z_$][\w$]*$/;
|
|
14
|
+
const IDENTIFIER$1 = /^[A-Za-z_$][\w$]*$/;
|
|
120
15
|
function unwrap(node) {
|
|
121
16
|
while (node && (node.type === "TSAsExpression" || node.type === "TSSatisfiesExpression" || node.type === "ParenthesizedExpression")) node = node.expression;
|
|
122
17
|
return node;
|
|
@@ -171,7 +66,7 @@ function locateLocalesObject(declaration) {
|
|
|
171
66
|
}
|
|
172
67
|
/** Quotes a property key only when it is not a valid identifier. */
|
|
173
68
|
function keyCode(key) {
|
|
174
|
-
return IDENTIFIER.test(key) ? key : JSON.stringify(key);
|
|
69
|
+
return IDENTIFIER$1.test(key) ? key : JSON.stringify(key);
|
|
175
70
|
}
|
|
176
71
|
/** Builds a detached `key: <value>` property node from source text. */
|
|
177
72
|
function makeProp(key, valueCode) {
|
|
@@ -459,15 +354,84 @@ function checkIcu(dictionary, strings, defaultLocale) {
|
|
|
459
354
|
return findings;
|
|
460
355
|
}
|
|
461
356
|
//#endregion
|
|
357
|
+
//#region src/utils/lockfile.ts
|
|
358
|
+
const LOCK_FILE = "dialex.lock.json";
|
|
359
|
+
const emptyLock = (sourceLocale) => ({
|
|
360
|
+
version: 1,
|
|
361
|
+
...sourceLocale ? { sourceLocale } : {},
|
|
362
|
+
locales: {}
|
|
363
|
+
});
|
|
364
|
+
const lockPath = (root) => path.join(root, LOCK_FILE);
|
|
365
|
+
/** A short, stable fingerprint of a source string. */
|
|
366
|
+
function hashSource(text) {
|
|
367
|
+
return createHash("sha1").update(text).digest("hex").slice(0, 8);
|
|
368
|
+
}
|
|
369
|
+
/** The lock file, or `undefined` when there is none (or it cannot be read). */
|
|
370
|
+
function readLock(root) {
|
|
371
|
+
try {
|
|
372
|
+
const data = JSON.parse(fs.readFileSync(lockPath(root), "utf-8"));
|
|
373
|
+
if (data?.version !== 1 || typeof data.locales !== "object" || data.locales === null) return;
|
|
374
|
+
return {
|
|
375
|
+
version: 1,
|
|
376
|
+
...typeof data.sourceLocale === "string" ? { sourceLocale: data.sourceLocale } : {},
|
|
377
|
+
locales: data.locales
|
|
378
|
+
};
|
|
379
|
+
} catch {
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
/** Writes the lock with sorted keys, so it produces small, stable diffs. */
|
|
384
|
+
function writeLock(root, lock) {
|
|
385
|
+
const sorted = {
|
|
386
|
+
version: 1,
|
|
387
|
+
...lock.sourceLocale ? { sourceLocale: lock.sourceLocale } : {},
|
|
388
|
+
locales: Object.fromEntries(Object.keys(lock.locales).sort().map((locale) => [locale, Object.fromEntries(Object.entries(lock.locales[locale]).sort(([a], [b]) => a.localeCompare(b)))]))
|
|
389
|
+
};
|
|
390
|
+
fs.writeFileSync(lockPath(root), JSON.stringify(sorted, null, 2) + "\n", "utf-8");
|
|
391
|
+
}
|
|
392
|
+
/** Notes that `key` in `locale` was written from this source text. */
|
|
393
|
+
function record(lock, locale, key, sourceText) {
|
|
394
|
+
(lock.locales[locale] ??= {})[key] = hashSource(sourceText);
|
|
395
|
+
}
|
|
396
|
+
/**
|
|
397
|
+
* True when `key` was recorded for `locale` from different source text than `sourceText`. A key
|
|
398
|
+
* with no record is not stale.
|
|
399
|
+
*/
|
|
400
|
+
function isStale(lock, locale, key, sourceText) {
|
|
401
|
+
const recorded = lock.locales[locale]?.[key];
|
|
402
|
+
return recorded !== void 0 && recorded !== hashSource(sourceText);
|
|
403
|
+
}
|
|
404
|
+
/**
|
|
405
|
+
* The lock to use for a run translating from `sourceLocale`. A lock written from another source
|
|
406
|
+
* locale says nothing about this one, so it is replaced by a fresh one.
|
|
407
|
+
*/
|
|
408
|
+
function lockFor(root, sourceLocale) {
|
|
409
|
+
const existing = readLock(root);
|
|
410
|
+
if (!existing) return {
|
|
411
|
+
lock: emptyLock(sourceLocale),
|
|
412
|
+
replaced: false
|
|
413
|
+
};
|
|
414
|
+
if (existing.sourceLocale && existing.sourceLocale !== sourceLocale) return {
|
|
415
|
+
lock: emptyLock(sourceLocale),
|
|
416
|
+
replaced: true
|
|
417
|
+
};
|
|
418
|
+
existing.sourceLocale = sourceLocale;
|
|
419
|
+
return {
|
|
420
|
+
lock: existing,
|
|
421
|
+
replaced: false
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
//#endregion
|
|
462
425
|
//#region src/analysis.ts
|
|
463
|
-
const DICTIONARY_IGNORE = [
|
|
426
|
+
const DICTIONARY_IGNORE$1 = [
|
|
464
427
|
"**/node_modules/**",
|
|
465
428
|
"**/dist/**",
|
|
466
|
-
"**/.next/**"
|
|
429
|
+
"**/.next/**",
|
|
430
|
+
"**/dialex.locales/**"
|
|
467
431
|
];
|
|
468
432
|
const DEFAULT_SOURCE_GLOB = "**/*.{ts,tsx,js,jsx,mjs,cjs,vue,svelte,astro,mdx}";
|
|
469
433
|
const SOURCE_IGNORE = [
|
|
470
|
-
...DICTIONARY_IGNORE,
|
|
434
|
+
...DICTIONARY_IGNORE$1,
|
|
471
435
|
"**/.nuxt/**",
|
|
472
436
|
"**/.output/**",
|
|
473
437
|
"**/.svelte-kit/**",
|
|
@@ -479,8 +443,8 @@ const SOURCE_IGNORE = [
|
|
|
479
443
|
"**/dialex.config.*",
|
|
480
444
|
"**/i18n.config.*"
|
|
481
445
|
];
|
|
482
|
-
/** `getDictionary("x")`, `useDictionary("x")`, `@
|
|
483
|
-
const DICTIONARY_CALL = /\b(?:getDictionary|useDictionary|
|
|
446
|
+
/** `getDictionary("x")`, `useDictionary("x")`, `@DialexDictionary("x")` */
|
|
447
|
+
const DICTIONARY_CALL = /\b(?:getDictionary|useDictionary|DialexDictionary)\(\s*(["'`])([^"'`$\n]+)\1/dg;
|
|
484
448
|
/** `t("dictionary.path.to.key")` */
|
|
485
449
|
const T_CALL = /(?<![\w$.])t\(\s*(["'`])([\w-]+(?:\.[\w-]+)+)\1/dg;
|
|
486
450
|
/** Finds `getDictionary("x")`-style and `t("x.y.z")` references in source text. */
|
|
@@ -622,10 +586,11 @@ async function analyzeProject(options) {
|
|
|
622
586
|
const files = fg.sync(include, {
|
|
623
587
|
cwd: root,
|
|
624
588
|
absolute: true,
|
|
625
|
-
ignore: [...DICTIONARY_IGNORE, ...extraIgnore]
|
|
589
|
+
ignore: [...DICTIONARY_IGNORE$1, ...extraIgnore]
|
|
626
590
|
});
|
|
627
591
|
const issues = [];
|
|
628
592
|
const dictionaries = [];
|
|
593
|
+
const lock = options.lock === false ? void 0 : readLock(root);
|
|
629
594
|
for (const file of files) {
|
|
630
595
|
const loaded = await loadDictionaryData(file, options.runtime === true);
|
|
631
596
|
if (!loaded.ok) {
|
|
@@ -691,6 +656,26 @@ async function analyzeProject(options) {
|
|
|
691
656
|
});
|
|
692
657
|
}
|
|
693
658
|
}
|
|
659
|
+
const sourceLeaves = lock?.sourceLocale ? data.leaves.get(lock.sourceLocale) : void 0;
|
|
660
|
+
if (lock && sourceLeaves) for (const locale of data.locales) {
|
|
661
|
+
if (locale === lock.sourceLocale) continue;
|
|
662
|
+
for (const [leafPath, leaf] of data.leaves.get(locale)) {
|
|
663
|
+
if (leaf.kind !== "string" || leaf.value === void 0) continue;
|
|
664
|
+
if (leaf.value.startsWith("[TODO] ")) continue;
|
|
665
|
+
const source = sourceLeaves.get(leafPath);
|
|
666
|
+
if (source?.kind !== "string" || source.value === void 0) continue;
|
|
667
|
+
if (!isStale(lock, locale, `${data.name}.${leafPath}`, source.value)) continue;
|
|
668
|
+
add({
|
|
669
|
+
level: "warning",
|
|
670
|
+
code: "stale-translation",
|
|
671
|
+
message: `Locale "${locale}" key "${leafPath}" is out of date: the "${lock.sourceLocale}" text changed after it was translated`,
|
|
672
|
+
range: at(locale, leafPath.split(".")),
|
|
673
|
+
locale,
|
|
674
|
+
path: leafPath.split("."),
|
|
675
|
+
sourceLocale: lock.sourceLocale
|
|
676
|
+
});
|
|
677
|
+
}
|
|
678
|
+
}
|
|
694
679
|
for (const finding of checkIcu(data.name, strings, defaultLocale)) {
|
|
695
680
|
const keyPath = finding.key.slice(data.name.length + 1).split(".");
|
|
696
681
|
add({
|
|
@@ -801,6 +786,186 @@ function relativeFile(root, file) {
|
|
|
801
786
|
return path.relative(root, file);
|
|
802
787
|
}
|
|
803
788
|
//#endregion
|
|
789
|
+
//#region src/utils/logger.ts
|
|
790
|
+
const logger = {
|
|
791
|
+
info: (msg) => console.log(pc.blue("ℹ ") + msg),
|
|
792
|
+
success: (msg) => console.log(pc.green("✔ ") + pc.bold(msg)),
|
|
793
|
+
warn: (msg) => console.warn(pc.yellow("⚠ ") + msg),
|
|
794
|
+
error: (msg) => console.error(pc.red("✖ ") + pc.bold(msg)),
|
|
795
|
+
log: (msg) => console.log(msg)
|
|
796
|
+
};
|
|
797
|
+
//#endregion
|
|
798
|
+
//#region src/utils/message-arguments.ts
|
|
799
|
+
const IDENTIFIER = /^[A-Za-z_$][\w$]*$/;
|
|
800
|
+
/**
|
|
801
|
+
* The TypeScript type of one ICU argument. A name can appear more than once (in a `plural` and as
|
|
802
|
+
* a plain `{name}`, say); the narrowest use wins, because that is the value the message needs.
|
|
803
|
+
*/
|
|
804
|
+
function typeOfArgument(uses) {
|
|
805
|
+
if (uses.some((u) => u.type === "plural" || u.type === "selectordinal" || u.type === "number")) return "number";
|
|
806
|
+
if (uses.some((u) => u.type === "date" || u.type === "time")) return "Date | number";
|
|
807
|
+
const select = uses.find((u) => u.type === "select");
|
|
808
|
+
if (select) {
|
|
809
|
+
const options = (select.selectors ?? []).filter((s) => s !== "other");
|
|
810
|
+
return options.length > 0 ? `${options.map((o) => JSON.stringify(o)).join(" | ")} | (string & {})` : "string";
|
|
811
|
+
}
|
|
812
|
+
return "string | number | boolean | Date";
|
|
813
|
+
}
|
|
814
|
+
/** `{ count: number; name: string | number | boolean | Date }` for the arguments of one message. */
|
|
815
|
+
function messageType(message) {
|
|
816
|
+
if (!message.includes("{")) return void 0;
|
|
817
|
+
let args;
|
|
818
|
+
try {
|
|
819
|
+
args = getArguments(parseMessage(message));
|
|
820
|
+
} catch {
|
|
821
|
+
return;
|
|
822
|
+
}
|
|
823
|
+
if (args.length === 0) return void 0;
|
|
824
|
+
const byName = /* @__PURE__ */ new Map();
|
|
825
|
+
for (const arg of args) byName.set(arg.name, [...byName.get(arg.name) ?? [], arg]);
|
|
826
|
+
return `{ ${[...byName].map(([name, uses]) => `${IDENTIFIER.test(name) ? name : JSON.stringify(name)}: ${typeOfArgument(uses)}`).join("; ")} }`;
|
|
827
|
+
}
|
|
828
|
+
/**
|
|
829
|
+
* `dictionary.key.path` to the type of its ICU values, read from each dictionary's reference
|
|
830
|
+
* locale (the default locale when the dictionary has it, otherwise its first). Other locales must
|
|
831
|
+
* use the same arguments, which `dx check` verifies.
|
|
832
|
+
*/
|
|
833
|
+
function messageArguments(dictionaries, defaultLocale) {
|
|
834
|
+
const result = {};
|
|
835
|
+
for (const df of dictionaries) {
|
|
836
|
+
if (!df?.name) continue;
|
|
837
|
+
const locales = listLocales(df);
|
|
838
|
+
const reference = locales.includes(defaultLocale) ? defaultLocale : locales[0];
|
|
839
|
+
if (!reference) continue;
|
|
840
|
+
for (const leaf of listLeaves(df, reference)) {
|
|
841
|
+
if (leaf.kind !== "string" || leaf.value === void 0) continue;
|
|
842
|
+
const type = messageType(leaf.value);
|
|
843
|
+
if (type) result[`${df.name}.${leaf.path.join(".")}`] = type;
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
return result;
|
|
847
|
+
}
|
|
848
|
+
//#endregion
|
|
849
|
+
//#region src/utils/split-locales.ts
|
|
850
|
+
const SKIPPED_KEYS = /* @__PURE__ */ new Set([
|
|
851
|
+
"loc",
|
|
852
|
+
"start",
|
|
853
|
+
"end",
|
|
854
|
+
"extra",
|
|
855
|
+
"range",
|
|
856
|
+
"comments",
|
|
857
|
+
"leadingComments",
|
|
858
|
+
"trailingComments",
|
|
859
|
+
"innerComments",
|
|
860
|
+
"tokens"
|
|
861
|
+
]);
|
|
862
|
+
/** Every identifier name used in a node, except property names that are not variable references. */
|
|
863
|
+
function usedNames(node, found = /* @__PURE__ */ new Set()) {
|
|
864
|
+
if (!node || typeof node !== "object") return found;
|
|
865
|
+
if (Array.isArray(node)) {
|
|
866
|
+
node.forEach((child) => usedNames(child, found));
|
|
867
|
+
return found;
|
|
868
|
+
}
|
|
869
|
+
const ast = node;
|
|
870
|
+
if (ast.type === "Identifier") found.add(ast.name);
|
|
871
|
+
for (const key of Object.keys(ast)) {
|
|
872
|
+
if (SKIPPED_KEYS.has(key)) continue;
|
|
873
|
+
const isPropertyName = key === "key" && !ast.computed && [
|
|
874
|
+
"ObjectProperty",
|
|
875
|
+
"ObjectMethod",
|
|
876
|
+
"TSPropertySignature",
|
|
877
|
+
"TSMethodSignature"
|
|
878
|
+
].includes(ast.type);
|
|
879
|
+
const isMemberName = key === "property" && !ast.computed && (ast.type === "MemberExpression" || ast.type === "OptionalMemberExpression");
|
|
880
|
+
if (isPropertyName || isMemberName) continue;
|
|
881
|
+
usedNames(ast[key], found);
|
|
882
|
+
}
|
|
883
|
+
return found;
|
|
884
|
+
}
|
|
885
|
+
/** Names bound anywhere in a pattern (`const { a, b: [c] } = ...`). */
|
|
886
|
+
function patternNames(pattern, found) {
|
|
887
|
+
if (!pattern) return;
|
|
888
|
+
switch (pattern.type) {
|
|
889
|
+
case "Identifier":
|
|
890
|
+
found.add(pattern.name);
|
|
891
|
+
break;
|
|
892
|
+
case "ObjectPattern":
|
|
893
|
+
for (const prop of pattern.properties) patternNames(prop.value ?? prop.argument, found);
|
|
894
|
+
break;
|
|
895
|
+
case "ArrayPattern":
|
|
896
|
+
for (const element of pattern.elements) patternNames(element ?? void 0, found);
|
|
897
|
+
break;
|
|
898
|
+
case "AssignmentPattern":
|
|
899
|
+
patternNames(pattern.left, found);
|
|
900
|
+
break;
|
|
901
|
+
case "RestElement": patternNames(pattern.argument, found);
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
/** What the file declares or imports at the top level: a locale that uses any of it cannot move. */
|
|
905
|
+
function topLevelNames(df) {
|
|
906
|
+
const names = /* @__PURE__ */ new Set();
|
|
907
|
+
const program = df.mod.$ast;
|
|
908
|
+
const statements = program?.body ?? program?.program?.body ?? [];
|
|
909
|
+
for (const raw of statements) {
|
|
910
|
+
const statement = raw.type === "ExportNamedDeclaration" && raw.declaration ? raw.declaration : raw;
|
|
911
|
+
switch (statement.type) {
|
|
912
|
+
case "ImportDeclaration":
|
|
913
|
+
for (const specifier of statement.specifiers) names.add(specifier.local.name);
|
|
914
|
+
break;
|
|
915
|
+
case "VariableDeclaration":
|
|
916
|
+
for (const declarator of statement.declarations) patternNames(declarator.id, names);
|
|
917
|
+
break;
|
|
918
|
+
case "FunctionDeclaration":
|
|
919
|
+
case "ClassDeclaration":
|
|
920
|
+
case "TSInterfaceDeclaration":
|
|
921
|
+
case "TSTypeAliasDeclaration":
|
|
922
|
+
case "TSEnumDeclaration": if (statement.id?.name) names.add(statement.id.name);
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
return names;
|
|
926
|
+
}
|
|
927
|
+
/**
|
|
928
|
+
* Cuts a dictionary into one self-contained piece per locale, for `lazy: "locale"`.
|
|
929
|
+
*
|
|
930
|
+
* A piece moves to its own file, so it must not depend on anything else in the original file.
|
|
931
|
+
* That is checked conservatively: if a locale uses any name the file imports or declares (even one
|
|
932
|
+
* that is only shadowed), the whole file is left alone. A missed split costs bytes; a wrong split
|
|
933
|
+
* would break the app.
|
|
934
|
+
*/
|
|
935
|
+
function splitLocales(df) {
|
|
936
|
+
if (!df.complete) return {
|
|
937
|
+
ok: false,
|
|
938
|
+
reason: "it uses spreads, computed keys or non-literal locale values"
|
|
939
|
+
};
|
|
940
|
+
const bound = topLevelNames(df);
|
|
941
|
+
const slices = [];
|
|
942
|
+
for (const prop of df.localesObj.properties) {
|
|
943
|
+
const locale = keyName(prop);
|
|
944
|
+
const value = unwrap(prop.value);
|
|
945
|
+
if (locale === void 0 || !isObject(value)) return {
|
|
946
|
+
ok: false,
|
|
947
|
+
reason: "a locale is not an object literal"
|
|
948
|
+
};
|
|
949
|
+
const clash = [...usedNames(value)].find((name) => bound.has(name));
|
|
950
|
+
if (clash) return {
|
|
951
|
+
ok: false,
|
|
952
|
+
reason: `"${locale}" uses "${clash}", which is defined elsewhere in the file`
|
|
953
|
+
};
|
|
954
|
+
slices.push({
|
|
955
|
+
locale,
|
|
956
|
+
code: generateCode(value).code
|
|
957
|
+
});
|
|
958
|
+
}
|
|
959
|
+
if (slices.length === 0) return {
|
|
960
|
+
ok: false,
|
|
961
|
+
reason: "it defines no locales"
|
|
962
|
+
};
|
|
963
|
+
return {
|
|
964
|
+
ok: true,
|
|
965
|
+
slices
|
|
966
|
+
};
|
|
967
|
+
}
|
|
968
|
+
//#endregion
|
|
804
969
|
//#region src/utils/static-config.ts
|
|
805
970
|
const CONFIG_NAMES = ["dialex.config", "i18n.config"];
|
|
806
971
|
const EXTENSIONS = [
|
|
@@ -865,7 +1030,7 @@ function apply(object, config, notes) {
|
|
|
865
1030
|
accept("include", (v) => typeof v === "string" || isStringArray(v), "a string or array of strings");
|
|
866
1031
|
accept("fallbacks", (v) => typeof v === "object" && v !== null && Object.values(v).every((list) => isStringArray(list)), "an object of string arrays");
|
|
867
1032
|
accept("prefixDefault", (v) => typeof v === "boolean", "true or false");
|
|
868
|
-
accept("lazy", (v) => typeof v === "boolean", "true or
|
|
1033
|
+
accept("lazy", (v) => typeof v === "boolean" || v === "locale", "true, false or \"locale\"");
|
|
869
1034
|
}
|
|
870
1035
|
/** Reads a config file's source text. Exposed for tests and for unsaved buffers. */
|
|
871
1036
|
function parseStaticConfig(text, file = "dialex.config.ts") {
|
|
@@ -925,6 +1090,313 @@ function readStaticConfig(root, configFile) {
|
|
|
925
1090
|
};
|
|
926
1091
|
}
|
|
927
1092
|
//#endregion
|
|
1093
|
+
//#region src/utils/tsconfig.ts
|
|
1094
|
+
/** Parses tsconfig-style JSON: comments and trailing commas are allowed. */
|
|
1095
|
+
function parseJsonc(text) {
|
|
1096
|
+
let out = "";
|
|
1097
|
+
let inString = false;
|
|
1098
|
+
for (let i = 0; i < text.length; i++) {
|
|
1099
|
+
const c = text[i];
|
|
1100
|
+
if (inString) {
|
|
1101
|
+
out += c;
|
|
1102
|
+
if (c === "\\") out += text[++i] ?? "";
|
|
1103
|
+
else if (c === "\"") inString = false;
|
|
1104
|
+
} else if (c === "\"") {
|
|
1105
|
+
inString = true;
|
|
1106
|
+
out += c;
|
|
1107
|
+
} else if (c === "/" && text[i + 1] === "/") {
|
|
1108
|
+
while (i < text.length && text[i] !== "\n") i++;
|
|
1109
|
+
out += "\n";
|
|
1110
|
+
} else if (c === "/" && text[i + 1] === "*") {
|
|
1111
|
+
i += 2;
|
|
1112
|
+
while (i < text.length && !(text[i] === "*" && text[i + 1] === "/")) i++;
|
|
1113
|
+
i++;
|
|
1114
|
+
} else out += c;
|
|
1115
|
+
}
|
|
1116
|
+
return JSON.parse(out.replace(/,(\s*[}\]])/g, "$1"));
|
|
1117
|
+
}
|
|
1118
|
+
function configFile(from) {
|
|
1119
|
+
if (fs.existsSync(from) && fs.statSync(from).isFile()) return from;
|
|
1120
|
+
for (const candidate of [`${from}.json`, path.join(from, "tsconfig.json")]) if (fs.existsSync(candidate) && fs.statSync(candidate).isFile()) return candidate;
|
|
1121
|
+
}
|
|
1122
|
+
/** `moduleResolution` and `include` of a tsconfig, following relative `extends`. */
|
|
1123
|
+
function read(file, depth = 0) {
|
|
1124
|
+
const result = { references: [] };
|
|
1125
|
+
let json;
|
|
1126
|
+
try {
|
|
1127
|
+
json = parseJsonc(fs.readFileSync(file, "utf-8"));
|
|
1128
|
+
} catch {
|
|
1129
|
+
return result;
|
|
1130
|
+
}
|
|
1131
|
+
const dir = path.dirname(file);
|
|
1132
|
+
const parent = typeof json.extends === "string" && json.extends.startsWith(".") && depth < 5 ? configFile(path.resolve(dir, json.extends)) : void 0;
|
|
1133
|
+
if (parent) Object.assign(result, read(parent, depth + 1));
|
|
1134
|
+
const options = json.compilerOptions ?? {};
|
|
1135
|
+
const module = typeof options.module === "string" ? options.module.toLowerCase() : void 0;
|
|
1136
|
+
const resolution = typeof options.moduleResolution === "string" ? options.moduleResolution.toLowerCase() : module === "node16" || module === "nodenext" ? module : void 0;
|
|
1137
|
+
if (resolution) result.moduleResolution = resolution;
|
|
1138
|
+
if (Array.isArray(json.include)) result.include = json.include;
|
|
1139
|
+
result.references = (json.references ?? []).map((ref) => typeof ref?.path === "string" ? configFile(path.resolve(dir, ref.path)) : void 0).filter(Boolean);
|
|
1140
|
+
return result;
|
|
1141
|
+
}
|
|
1142
|
+
/**
|
|
1143
|
+
* The extension for relative imports in generated files. TypeScript's `node16`/`nodenext` need
|
|
1144
|
+
* `./x.js`; bundlers (webpack, Vite) resolve extensionless imports, and webpack cannot map `.js`
|
|
1145
|
+
* to a `.ts` file. Without any tsconfig it is plain Node ESM, which needs the extension.
|
|
1146
|
+
*/
|
|
1147
|
+
function relativeImportExtension(root) {
|
|
1148
|
+
const file = configFile(path.join(root, "tsconfig.json"));
|
|
1149
|
+
if (!file) return ".js";
|
|
1150
|
+
let resolved = read(file);
|
|
1151
|
+
if (!resolved.moduleResolution && resolved.references.length > 0) {
|
|
1152
|
+
const projects = resolved.references.map((ref) => read(ref));
|
|
1153
|
+
resolved = projects.find((p) => p.include?.some((entry) => /^(\.\/)?src(\/|$)/.test(entry))) ?? projects[0] ?? resolved;
|
|
1154
|
+
}
|
|
1155
|
+
const resolution = resolved.moduleResolution;
|
|
1156
|
+
return resolution === "node16" || resolution === "nodenext" ? ".js" : "";
|
|
1157
|
+
}
|
|
1158
|
+
//#endregion
|
|
1159
|
+
//#region src/commands/generate.ts
|
|
1160
|
+
const DICTIONARY_IGNORE = [
|
|
1161
|
+
"**/node_modules/**",
|
|
1162
|
+
"**/dist/**",
|
|
1163
|
+
"**/.next/**",
|
|
1164
|
+
"**/dialex.locales/**"
|
|
1165
|
+
];
|
|
1166
|
+
/** The folder, next to `dialex.generated.ts`, that holds the per-locale modules. */
|
|
1167
|
+
const LOCALES_DIR = "dialex.locales";
|
|
1168
|
+
const safeName = (text) => text.replace(/[^A-Za-z0-9._-]/g, "_");
|
|
1169
|
+
/**
|
|
1170
|
+
* Works out `dialex.generated.ts` and `dialex-env.d.ts` for a project without writing anything,
|
|
1171
|
+
* so `check` can tell whether they are up to date.
|
|
1172
|
+
*
|
|
1173
|
+
* Settings come from `dialex.config.*` and fall back to {@link DEFAULT_CONFIG}; `locales` falls
|
|
1174
|
+
* back to the locales the dictionaries define.
|
|
1175
|
+
*/
|
|
1176
|
+
function renderGenerated(root, options = {}) {
|
|
1177
|
+
const inlineConfig = options.config ? { configFile: options.config } : {};
|
|
1178
|
+
const staticRead = readStaticConfig(root, options.config);
|
|
1179
|
+
const config = options.static ? staticRead.config : resolveDialexConfig(root, inlineConfig);
|
|
1180
|
+
const include = config.include || DEFAULT_CONFIG.include;
|
|
1181
|
+
const found = fg.sync(include, {
|
|
1182
|
+
cwd: root,
|
|
1183
|
+
absolute: true,
|
|
1184
|
+
ignore: DICTIONARY_IGNORE
|
|
1185
|
+
}).sort();
|
|
1186
|
+
const srcDir = fs.existsSync(path.join(root, "src")) ? path.join(root, "src") : root;
|
|
1187
|
+
const outputPath = options.output ? path.resolve(root, options.output) : path.join(srcDir, "dialex.generated.ts");
|
|
1188
|
+
const outputDir = path.dirname(outputPath);
|
|
1189
|
+
const files = found.filter((file) => file !== outputPath && !file.endsWith(".d.ts") && path.basename(file) !== "dialex.generated.ts");
|
|
1190
|
+
const parsed = files.map((file) => parseDictionaryText(file, fs.readFileSync(file, "utf-8")));
|
|
1191
|
+
const inferred = [];
|
|
1192
|
+
for (const df of parsed) if (df) {
|
|
1193
|
+
for (const locale of listLocales(df)) if (!inferred.includes(locale)) inferred.push(locale);
|
|
1194
|
+
}
|
|
1195
|
+
const locales = config.locales?.length ? config.locales : inferred;
|
|
1196
|
+
const clientConfig = {
|
|
1197
|
+
defaultLocale: config.defaultLocale || DEFAULT_CONFIG.defaultLocale,
|
|
1198
|
+
locales,
|
|
1199
|
+
fallbacks: config.fallbacks ?? DEFAULT_CONFIG.fallbacks,
|
|
1200
|
+
prefixDefault: config.prefixDefault ?? DEFAULT_CONFIG.prefixDefault,
|
|
1201
|
+
lazy: config.lazy ?? DEFAULT_CONFIG.lazy
|
|
1202
|
+
};
|
|
1203
|
+
const extension = relativeImportExtension(root);
|
|
1204
|
+
const importPath = (file) => {
|
|
1205
|
+
let rel = path.relative(outputDir, file).replace(/\\/g, "/").replace(/\.ts$/, extension);
|
|
1206
|
+
if (!rel.startsWith(".")) rel = "./" + rel;
|
|
1207
|
+
return rel;
|
|
1208
|
+
};
|
|
1209
|
+
const header = `// Auto-generated by dialex CLI. Do not edit directly.
|
|
1210
|
+
/* eslint-disable */
|
|
1211
|
+
// @ts-nocheck`;
|
|
1212
|
+
const configExport = `export const config = ${JSON.stringify(clientConfig, null, 2).replace("\"lazy\": \"locale\"", "\"lazy\": \"locale\" as const")};
|
|
1213
|
+
|
|
1214
|
+
${locales.length > 0 ? `export const locales = ${JSON.stringify(locales)} as const;\nexport type Locale = (typeof locales)[number];` : "export const locales: readonly string[] = [];\nexport type Locale = string;"}`;
|
|
1215
|
+
const extraFiles = [];
|
|
1216
|
+
const notes = [];
|
|
1217
|
+
const takenNames = /* @__PURE__ */ new Set();
|
|
1218
|
+
let content;
|
|
1219
|
+
if (clientConfig.lazy) content = `${header}
|
|
1220
|
+
|
|
1221
|
+
export const dictionaries = [];
|
|
1222
|
+
|
|
1223
|
+
export const dictionaryMap = {};
|
|
1224
|
+
|
|
1225
|
+
export const loaders = {
|
|
1226
|
+
${files.map((file, i) => {
|
|
1227
|
+
const df = parsed[i];
|
|
1228
|
+
const name = df?.name;
|
|
1229
|
+
if (!name || !df) throw new Error(`Cannot determine the dictionary name of ${path.relative(root, file)} for lazy loading. Use defineDictionary("name", ...) or a literal \`name\` property.`);
|
|
1230
|
+
const whole = ` ${JSON.stringify(name)}: () => import("${importPath(file)}"),`;
|
|
1231
|
+
if (clientConfig.lazy !== "locale") return whole;
|
|
1232
|
+
const split = splitLocales(df);
|
|
1233
|
+
if (!split.ok) {
|
|
1234
|
+
notes.push(`${path.relative(root, file)} is loaded whole, not per locale: ${split.reason}.`);
|
|
1235
|
+
return whole;
|
|
1236
|
+
}
|
|
1237
|
+
let base = safeName(name);
|
|
1238
|
+
for (let n = 2; takenNames.has(base); n++) base = `${safeName(name)}-${n}`;
|
|
1239
|
+
takenNames.add(base);
|
|
1240
|
+
const entries = split.slices.map((slice) => {
|
|
1241
|
+
const target = path.join(outputDir, LOCALES_DIR, `${base}.${safeName(slice.locale)}.ts`);
|
|
1242
|
+
extraFiles.push({
|
|
1243
|
+
path: target,
|
|
1244
|
+
content: `${header}\nexport default ${slice.code};\n`
|
|
1245
|
+
});
|
|
1246
|
+
return ` ${JSON.stringify(slice.locale)}: () => import("${importPath(target)}"),`;
|
|
1247
|
+
});
|
|
1248
|
+
return ` ${JSON.stringify(name)}: {\n${entries.join("\n")}\n },`;
|
|
1249
|
+
}).join("\n")}
|
|
1250
|
+
};
|
|
1251
|
+
|
|
1252
|
+
${configExport}
|
|
1253
|
+
|
|
1254
|
+
export const dialex = { dictionaries, config, loaders };
|
|
1255
|
+
|
|
1256
|
+
export default dictionaries;
|
|
1257
|
+
`;
|
|
1258
|
+
else {
|
|
1259
|
+
const ids = files.map((_, i) => `dict_${i}`);
|
|
1260
|
+
const dictList = ids.length <= 1 ? `[${ids.join(", ")}]` : `[\n ${ids.join(",\n ")}\n]`;
|
|
1261
|
+
content = `${header}
|
|
1262
|
+
${files.map((file, i) => `import ${ids[i]} from "${importPath(file)}";`).join("\n")}
|
|
1263
|
+
|
|
1264
|
+
export const dictionaries = ${dictList};
|
|
1265
|
+
|
|
1266
|
+
export const dictionaryMap = {
|
|
1267
|
+
${ids.map((id) => ` [${id}.name]: ${id}.dictionary,`).join("\n")}
|
|
1268
|
+
};
|
|
1269
|
+
|
|
1270
|
+
${configExport}
|
|
1271
|
+
|
|
1272
|
+
export const dialex = { dictionaries, config };
|
|
1273
|
+
|
|
1274
|
+
export default dictionaries;
|
|
1275
|
+
`;
|
|
1276
|
+
}
|
|
1277
|
+
const dts = renderDts(root, files, locales, messageArguments(parsed, clientConfig.defaultLocale));
|
|
1278
|
+
return {
|
|
1279
|
+
files,
|
|
1280
|
+
outputPath,
|
|
1281
|
+
content,
|
|
1282
|
+
dtsPath: dts?.path ?? path.join(srcDir, "dialex-env.d.ts"),
|
|
1283
|
+
dtsContent: dts?.content,
|
|
1284
|
+
configFile: staticRead.file,
|
|
1285
|
+
extraFiles,
|
|
1286
|
+
notes
|
|
1287
|
+
};
|
|
1288
|
+
}
|
|
1289
|
+
function writeIfChanged(file, content) {
|
|
1290
|
+
if ((fs.existsSync(file) ? fs.readFileSync(file, "utf-8") : "") === content) return;
|
|
1291
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
1292
|
+
fs.writeFileSync(file, content, "utf-8");
|
|
1293
|
+
}
|
|
1294
|
+
function generateDictionaries(root, options = {}) {
|
|
1295
|
+
const rendered = renderGenerated(root, options);
|
|
1296
|
+
if (rendered.dtsContent !== void 0) writeIfChanged(rendered.dtsPath, rendered.dtsContent);
|
|
1297
|
+
writeIfChanged(rendered.outputPath, rendered.content);
|
|
1298
|
+
const dir = path.join(path.dirname(rendered.outputPath), LOCALES_DIR);
|
|
1299
|
+
const wanted = new Set(rendered.extraFiles.map((f) => f.path));
|
|
1300
|
+
for (const file of rendered.extraFiles) writeIfChanged(file.path, file.content);
|
|
1301
|
+
if (fs.existsSync(dir)) {
|
|
1302
|
+
for (const entry of fs.readdirSync(dir)) {
|
|
1303
|
+
const full = path.join(dir, entry);
|
|
1304
|
+
if (entry.endsWith(".ts") && !wanted.has(full)) fs.rmSync(full);
|
|
1305
|
+
}
|
|
1306
|
+
if (fs.readdirSync(dir).length === 0) fs.rmdirSync(dir);
|
|
1307
|
+
}
|
|
1308
|
+
return {
|
|
1309
|
+
files: rendered.files,
|
|
1310
|
+
outputPath: rendered.outputPath,
|
|
1311
|
+
dtsPath: rendered.dtsPath,
|
|
1312
|
+
configFile: rendered.configFile,
|
|
1313
|
+
notes: rendered.notes
|
|
1314
|
+
};
|
|
1315
|
+
}
|
|
1316
|
+
async function runGenerate(options = {}) {
|
|
1317
|
+
const root = options.cwd || process.cwd();
|
|
1318
|
+
const quiet = options.json === true;
|
|
1319
|
+
const doGenerate = () => {
|
|
1320
|
+
try {
|
|
1321
|
+
const { files, outputPath, dtsPath, configFile, notes } = generateDictionaries(root, options);
|
|
1322
|
+
if (quiet) console.log(JSON.stringify({
|
|
1323
|
+
success: true,
|
|
1324
|
+
files: files.map((f) => path.relative(root, f)),
|
|
1325
|
+
outputPath: path.relative(root, outputPath),
|
|
1326
|
+
dtsPath: path.relative(root, dtsPath),
|
|
1327
|
+
notes
|
|
1328
|
+
}, null, 2));
|
|
1329
|
+
else {
|
|
1330
|
+
if (!configFile) logger.info("No dialex.config.* found, using defaults (run `dialex init` to add one).");
|
|
1331
|
+
logger.success(`Generated dictionaries for ${files.length} file${files.length === 1 ? "" : "s"}:`);
|
|
1332
|
+
logger.info(`→ ${path.relative(root, outputPath)}`);
|
|
1333
|
+
logger.info(`→ ${path.relative(root, dtsPath)}`);
|
|
1334
|
+
for (const note of notes) logger.info(note);
|
|
1335
|
+
}
|
|
1336
|
+
return true;
|
|
1337
|
+
} catch (err) {
|
|
1338
|
+
if (quiet) console.log(JSON.stringify({
|
|
1339
|
+
success: false,
|
|
1340
|
+
error: err.message || String(err)
|
|
1341
|
+
}, null, 2));
|
|
1342
|
+
else logger.error(`Generation failed: ${err.message || String(err)}`);
|
|
1343
|
+
return false;
|
|
1344
|
+
}
|
|
1345
|
+
};
|
|
1346
|
+
if (!doGenerate() && !options.watch) process.exit(1);
|
|
1347
|
+
if (options.watch) {
|
|
1348
|
+
logger.info("Watching for dictionary changes (-w, --watch)...");
|
|
1349
|
+
startGenerateWatcher(root, options, doGenerate);
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
const WATCH_IGNORED = /(^|[\\/])(node_modules|dist|\.next|\.nuxt|\.output|\.turbo|\.git|dialex\.locales)([\\/]|$)/;
|
|
1353
|
+
const CONFIG_FILE = /(^|[\\/])(dialex|i18n)\.config\.[cm]?[jt]s(on)?$/;
|
|
1354
|
+
/**
|
|
1355
|
+
* Regenerates when a dictionary or config file is added, edited or removed. chokidar 4+ no longer
|
|
1356
|
+
* understands globs, so this watches the project directory and decides for itself which events
|
|
1357
|
+
* matter, using the same `include` glob as `generate`.
|
|
1358
|
+
*/
|
|
1359
|
+
function startGenerateWatcher(root, options, regenerate, delay = 100) {
|
|
1360
|
+
const dictionaryFiles = () => new Set(renderGenerated(root, options).files);
|
|
1361
|
+
let known = dictionaryFiles();
|
|
1362
|
+
let outputs = /* @__PURE__ */ new Set();
|
|
1363
|
+
let timer;
|
|
1364
|
+
const trigger = () => {
|
|
1365
|
+
if (timer) clearTimeout(timer);
|
|
1366
|
+
timer = setTimeout(() => {
|
|
1367
|
+
regenerate();
|
|
1368
|
+
try {
|
|
1369
|
+
known = dictionaryFiles();
|
|
1370
|
+
const rendered = renderGenerated(root, options);
|
|
1371
|
+
outputs = /* @__PURE__ */ new Set([rendered.outputPath, rendered.dtsPath]);
|
|
1372
|
+
} catch {}
|
|
1373
|
+
}, delay);
|
|
1374
|
+
};
|
|
1375
|
+
const watcher = chokidar.watch(root, {
|
|
1376
|
+
ignoreInitial: true,
|
|
1377
|
+
ignored: (file) => WATCH_IGNORED.test(file)
|
|
1378
|
+
});
|
|
1379
|
+
watcher.on("all", (event, filePath) => {
|
|
1380
|
+
const file = path.resolve(root, filePath);
|
|
1381
|
+
if (outputs.has(file)) return;
|
|
1382
|
+
const isConfig = CONFIG_FILE.test(file);
|
|
1383
|
+
if (event === "change") {
|
|
1384
|
+
if (!known.has(file) && !isConfig) return;
|
|
1385
|
+
} else if (event === "add" || event === "unlink") {
|
|
1386
|
+
let now;
|
|
1387
|
+
try {
|
|
1388
|
+
now = dictionaryFiles();
|
|
1389
|
+
} catch {
|
|
1390
|
+
return;
|
|
1391
|
+
}
|
|
1392
|
+
if (!(now.size !== known.size || [...now].some((f) => !known.has(f))) && !isConfig) return;
|
|
1393
|
+
} else return;
|
|
1394
|
+
logger.info(`[${event}] ${path.relative(root, file)} changed, regenerating...`);
|
|
1395
|
+
trigger();
|
|
1396
|
+
});
|
|
1397
|
+
return watcher;
|
|
1398
|
+
}
|
|
1399
|
+
//#endregion
|
|
928
1400
|
//#region src/utils/project.ts
|
|
929
1401
|
async function loadDictionaries(root, config) {
|
|
930
1402
|
const include = config.include || "**/*.content.ts";
|
|
@@ -934,7 +1406,8 @@ async function loadDictionaries(root, config) {
|
|
|
934
1406
|
ignore: [
|
|
935
1407
|
"**/node_modules/**",
|
|
936
1408
|
"**/dist/**",
|
|
937
|
-
"**/.next/**"
|
|
1409
|
+
"**/.next/**",
|
|
1410
|
+
"**/dialex.locales/**"
|
|
938
1411
|
]
|
|
939
1412
|
});
|
|
940
1413
|
const dictionaries = [];
|
|
@@ -963,7 +1436,7 @@ async function loadDictionaries(root, config) {
|
|
|
963
1436
|
* every dictionary file for AST-level reading and editing.
|
|
964
1437
|
*/
|
|
965
1438
|
async function loadProject(root, configFile) {
|
|
966
|
-
const config =
|
|
1439
|
+
const config = resolveDialexConfig(root, configFile ? { configFile } : {});
|
|
967
1440
|
return {
|
|
968
1441
|
root,
|
|
969
1442
|
config,
|
|
@@ -987,6 +1460,6 @@ async function loadStaticProject(root, configFile) {
|
|
|
987
1460
|
/** `home` + `["nav", "about"]` -> `home.nav.about` */
|
|
988
1461
|
const toKey = (dictionary, keyPath) => [dictionary, ...keyPath].join(".");
|
|
989
1462
|
//#endregion
|
|
990
|
-
export {
|
|
1463
|
+
export { listLocales as A, TODO_PREFIX as C, getString as D, getLeafSource as E, saveDictionaryFile as F, setString as I, locationOf as M, parseDictionaryText as N, hasPath as O, renderDictionaryFile as P, writeLock as S, dictionaryLocation as T, isStale as _, renderGenerated as a, readLock as b, parseStaticConfig as c, DEFAULT_SOURCE_GLOB as d, analyzeProject as f, hashSource as g, LOCK_FILE as h, generateDictionaries as i, loadDictionaryFile as j, listLeaves as k, readStaticConfig as l, scanReferences as m, loadStaticProject as n, runGenerate as o, relativeFile as p, toKey as r, DEFAULT_STATIC_CONFIG as s, loadProject as t, logger as u, lockFor as v, copyLeaf as w, record as x, lockPath as y };
|
|
991
1464
|
|
|
992
|
-
//# sourceMappingURL=project-
|
|
1465
|
+
//# sourceMappingURL=project-BJYmnKiU.mjs.map
|