@dialexjs/cli 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +86 -3
- package/dist/api.d.mts +229 -0
- package/dist/api.d.mts.map +1 -0
- package/dist/api.mjs +2 -0
- package/dist/index.d.mts +1 -0
- package/dist/index.mjs +1088 -0
- package/dist/index.mjs.map +1 -0
- package/dist/project-D4PmfqxM.mjs +883 -0
- package/dist/project-D4PmfqxM.mjs.map +1 -0
- package/dist/translate/index.d.mts +92 -0
- package/dist/translate/index.d.mts.map +1 -0
- package/dist/translate/index.mjs +289 -0
- package/dist/translate/index.mjs.map +1 -0
- package/package.json +50 -4
|
@@ -0,0 +1,883 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import fg from "fast-glob";
|
|
4
|
+
import { resolveI18nConfig } from "dialexjs/scanner";
|
|
5
|
+
import { generateCode, parseExpression, parseModule, writeFile } from "magicast";
|
|
6
|
+
import { pathToFileURL } from "node:url";
|
|
7
|
+
import { argumentSignature, findPlurals, isIcuStructured, parseMessage } from "dialexjs/icu";
|
|
8
|
+
//#region src/utils/dictionary-edit.ts
|
|
9
|
+
const TODO_PREFIX = "[TODO] ";
|
|
10
|
+
const IDENTIFIER = /^[A-Za-z_$][\w$]*$/;
|
|
11
|
+
function unwrap(node) {
|
|
12
|
+
while (node && (node.type === "TSAsExpression" || node.type === "TSSatisfiesExpression" || node.type === "ParenthesizedExpression")) node = node.expression;
|
|
13
|
+
return node;
|
|
14
|
+
}
|
|
15
|
+
const isObject = (node) => node?.type === "ObjectExpression";
|
|
16
|
+
function keyName(prop) {
|
|
17
|
+
const key = prop?.key;
|
|
18
|
+
if (!key || prop.computed) return void 0;
|
|
19
|
+
if (key.type === "Identifier") return key.name;
|
|
20
|
+
if (key.type === "StringLiteral" || key.type === "Literal" && typeof key.value === "string") return key.value;
|
|
21
|
+
if (key.type === "NumericLiteral") return String(key.value);
|
|
22
|
+
}
|
|
23
|
+
const isPropLike = (p) => p?.type === "ObjectProperty" || p?.type === "Property" || p?.type === "ObjectMethod";
|
|
24
|
+
function findProp(obj, key) {
|
|
25
|
+
return obj.properties.find((p) => isPropLike(p) && keyName(p) === key);
|
|
26
|
+
}
|
|
27
|
+
function stringValue(node) {
|
|
28
|
+
node = unwrap(node);
|
|
29
|
+
if (node?.type === "StringLiteral") return node.value;
|
|
30
|
+
if (node?.type === "Literal" && typeof node.value === "string") return node.value;
|
|
31
|
+
if (node?.type === "TemplateLiteral" && node.expressions.length === 0) return node.quasis[0]?.value?.cooked ?? void 0;
|
|
32
|
+
}
|
|
33
|
+
function leafKind(node) {
|
|
34
|
+
node = unwrap(node);
|
|
35
|
+
if (stringValue(node) !== void 0) return "string";
|
|
36
|
+
if (node?.type === "ArrowFunctionExpression" || node?.type === "FunctionExpression" || node?.type === "ObjectMethod") return "function";
|
|
37
|
+
return "other";
|
|
38
|
+
}
|
|
39
|
+
function dictionaryProp(obj) {
|
|
40
|
+
const value = unwrap(findProp(obj, "dictionary")?.value);
|
|
41
|
+
return isObject(value) ? value : void 0;
|
|
42
|
+
}
|
|
43
|
+
function locateName(declaration) {
|
|
44
|
+
const decl = unwrap(declaration);
|
|
45
|
+
if (decl?.type === "CallExpression") {
|
|
46
|
+
const first = unwrap(decl.arguments[0]);
|
|
47
|
+
if (isObject(first)) return stringValue(findProp(first, "name")?.value);
|
|
48
|
+
return stringValue(first);
|
|
49
|
+
}
|
|
50
|
+
if (isObject(decl)) return stringValue(findProp(decl, "name")?.value);
|
|
51
|
+
}
|
|
52
|
+
function locateLocalesObject(declaration) {
|
|
53
|
+
const decl = unwrap(declaration);
|
|
54
|
+
if (decl?.type === "CallExpression") {
|
|
55
|
+
const first = unwrap(decl.arguments[0]);
|
|
56
|
+
const second = unwrap(decl.arguments[1]);
|
|
57
|
+
if (isObject(second)) return second;
|
|
58
|
+
if (isObject(first)) return dictionaryProp(first);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
if (isObject(decl)) return dictionaryProp(decl);
|
|
62
|
+
}
|
|
63
|
+
/** Quotes a property key only when it is not a valid identifier. */
|
|
64
|
+
function keyCode(key) {
|
|
65
|
+
return IDENTIFIER.test(key) ? key : JSON.stringify(key);
|
|
66
|
+
}
|
|
67
|
+
/** Builds a detached `key: <value>` property node from source text. */
|
|
68
|
+
function makeProp(key, valueCode) {
|
|
69
|
+
return parseExpression(`({ ${keyCode(key)}: ${valueCode} })`).$ast.properties[0];
|
|
70
|
+
}
|
|
71
|
+
function makeStringNode(value) {
|
|
72
|
+
return makeProp("_", JSON.stringify(value)).value;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* A dictionary is statically readable when every locale is an object literal and no object on the
|
|
76
|
+
* way down uses spreads or computed keys.
|
|
77
|
+
*/
|
|
78
|
+
function isStaticallyComplete(localesObj) {
|
|
79
|
+
const walk = (obj) => obj.properties.every((prop) => {
|
|
80
|
+
if (!isPropLike(prop) || keyName(prop) === void 0) return false;
|
|
81
|
+
if (prop.type === "ObjectMethod") return true;
|
|
82
|
+
const value = unwrap(prop.value);
|
|
83
|
+
return isObject(value) ? walk(value) : true;
|
|
84
|
+
});
|
|
85
|
+
return localesObj.properties.every((prop) => {
|
|
86
|
+
if (!isPropLike(prop) || keyName(prop) === void 0 || prop.type === "ObjectMethod") return false;
|
|
87
|
+
const value = unwrap(prop.value);
|
|
88
|
+
return isObject(value) && walk(value);
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Parses dictionary source text (for example an unsaved editor buffer). Returns `undefined` when
|
|
93
|
+
* the file does not have an editable shape; throws on syntax errors.
|
|
94
|
+
*/
|
|
95
|
+
function parseDictionaryText(file, text) {
|
|
96
|
+
const mod = parseModule(text);
|
|
97
|
+
const ast = mod.exports.default?.$ast;
|
|
98
|
+
if (!ast) return void 0;
|
|
99
|
+
const declaration = ast.declaration ?? ast;
|
|
100
|
+
const localesObj = locateLocalesObject(declaration);
|
|
101
|
+
return localesObj ? {
|
|
102
|
+
file,
|
|
103
|
+
mod,
|
|
104
|
+
localesObj,
|
|
105
|
+
name: locateName(declaration),
|
|
106
|
+
endsWithNewline: text.endsWith("\n"),
|
|
107
|
+
complete: isStaticallyComplete(localesObj)
|
|
108
|
+
} : void 0;
|
|
109
|
+
}
|
|
110
|
+
/** Loads a dictionary file; returns `undefined` when its shape is not editable. */
|
|
111
|
+
async function loadDictionaryFile(file) {
|
|
112
|
+
return parseDictionaryText(file, fs.readFileSync(file, "utf-8"));
|
|
113
|
+
}
|
|
114
|
+
/** The source text with all edits applied, without writing anything. */
|
|
115
|
+
function renderDictionaryFile(df) {
|
|
116
|
+
const code = generateCode(df.mod).code;
|
|
117
|
+
return df.endsWithNewline && !code.endsWith("\n") ? `${code}\n` : code;
|
|
118
|
+
}
|
|
119
|
+
async function saveDictionaryFile(df) {
|
|
120
|
+
await writeFile(df.mod, df.file);
|
|
121
|
+
if (df.endsWithNewline) {
|
|
122
|
+
const text = fs.readFileSync(df.file, "utf-8");
|
|
123
|
+
if (!text.endsWith("\n")) fs.writeFileSync(df.file, text + "\n", "utf-8");
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
function listLocales(df) {
|
|
127
|
+
return df.localesObj.properties.filter(isPropLike).map(keyName).filter((k) => k !== void 0);
|
|
128
|
+
}
|
|
129
|
+
function localeObject(df, locale) {
|
|
130
|
+
const value = unwrap(findProp(df.localesObj, locale)?.value);
|
|
131
|
+
return isObject(value) ? value : void 0;
|
|
132
|
+
}
|
|
133
|
+
/** Lists every leaf (string, function, or other value) of a locale, in source order. */
|
|
134
|
+
function listLeaves(df, locale) {
|
|
135
|
+
const root = localeObject(df, locale);
|
|
136
|
+
if (!root) return [];
|
|
137
|
+
const leaves = [];
|
|
138
|
+
const walk = (obj, prefix) => {
|
|
139
|
+
for (const prop of obj.properties) {
|
|
140
|
+
if (!isPropLike(prop)) continue;
|
|
141
|
+
const name = keyName(prop);
|
|
142
|
+
if (name === void 0) continue;
|
|
143
|
+
const path = [...prefix, name];
|
|
144
|
+
const value = prop.type === "ObjectMethod" ? prop : unwrap(prop.value);
|
|
145
|
+
if (isObject(value)) walk(value, path);
|
|
146
|
+
else {
|
|
147
|
+
const kind = leafKind(value);
|
|
148
|
+
leaves.push({
|
|
149
|
+
path,
|
|
150
|
+
kind,
|
|
151
|
+
...kind === "string" ? { value: stringValue(value) } : {}
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
walk(root, []);
|
|
157
|
+
return leaves;
|
|
158
|
+
}
|
|
159
|
+
function getProp(df, locale, path) {
|
|
160
|
+
let obj = localeObject(df, locale);
|
|
161
|
+
for (let i = 0; obj && i < path.length; i++) {
|
|
162
|
+
const prop = findProp(obj, path[i]);
|
|
163
|
+
if (!prop) return void 0;
|
|
164
|
+
if (i === path.length - 1) return prop;
|
|
165
|
+
const next = unwrap(prop.value);
|
|
166
|
+
obj = isObject(next) ? next : void 0;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
function hasPath(df, locale, path) {
|
|
170
|
+
return getProp(df, locale, path) !== void 0;
|
|
171
|
+
}
|
|
172
|
+
function getString(df, locale, path) {
|
|
173
|
+
const prop = getProp(df, locale, path);
|
|
174
|
+
return prop && prop.type !== "ObjectMethod" ? stringValue(prop.value) : void 0;
|
|
175
|
+
}
|
|
176
|
+
function ensureLocale(df, locale) {
|
|
177
|
+
let obj = localeObject(df, locale);
|
|
178
|
+
if (!obj) {
|
|
179
|
+
const prop = makeProp(locale, "{}");
|
|
180
|
+
df.localesObj.properties.push(prop);
|
|
181
|
+
obj = prop.value;
|
|
182
|
+
}
|
|
183
|
+
return obj;
|
|
184
|
+
}
|
|
185
|
+
/** Returns the object that should contain the last path segment, creating objects as needed. */
|
|
186
|
+
function ensureParent(df, locale, path) {
|
|
187
|
+
let obj = ensureLocale(df, locale);
|
|
188
|
+
for (const segment of path.slice(0, -1)) {
|
|
189
|
+
let prop = findProp(obj, segment);
|
|
190
|
+
if (!prop) {
|
|
191
|
+
prop = makeProp(segment, "{}");
|
|
192
|
+
obj.properties.push(prop);
|
|
193
|
+
}
|
|
194
|
+
const next = unwrap(prop.value);
|
|
195
|
+
if (!isObject(next)) return void 0;
|
|
196
|
+
obj = next;
|
|
197
|
+
}
|
|
198
|
+
return obj;
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Sets a string value at `path`. Existing function or non-string values are left alone
|
|
202
|
+
* (`"skipped"`) because they cannot be translated as plain text.
|
|
203
|
+
*/
|
|
204
|
+
function setString(df, locale, path, value) {
|
|
205
|
+
const parent = ensureParent(df, locale, path);
|
|
206
|
+
if (!parent) return "skipped";
|
|
207
|
+
const last = path[path.length - 1];
|
|
208
|
+
const existing = findProp(parent, last);
|
|
209
|
+
if (existing) {
|
|
210
|
+
if (existing.type === "ObjectMethod" || leafKind(existing.value) !== "string") return "skipped";
|
|
211
|
+
existing.value = makeStringNode(value);
|
|
212
|
+
return "updated";
|
|
213
|
+
}
|
|
214
|
+
parent.properties.push(makeProp(last, JSON.stringify(value)));
|
|
215
|
+
return "created";
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Copies the leaf at `path` from one locale to another. String values can be rewritten with
|
|
219
|
+
* `mapString` (for example to add a `[TODO]` marker); other values are copied verbatim.
|
|
220
|
+
* Returns `false` when the source has no such leaf or the target already has one.
|
|
221
|
+
*/
|
|
222
|
+
function copyLeaf(df, fromLocale, toLocale, path, mapString) {
|
|
223
|
+
const source = getProp(df, fromLocale, path);
|
|
224
|
+
if (!source || hasPath(df, toLocale, path)) return false;
|
|
225
|
+
const parent = ensureParent(df, toLocale, path);
|
|
226
|
+
if (!parent) return false;
|
|
227
|
+
const last = path[path.length - 1];
|
|
228
|
+
const text = source.type === "ObjectMethod" ? void 0 : stringValue(source.value);
|
|
229
|
+
if (text !== void 0 && mapString) parent.properties.push(makeProp(last, JSON.stringify(mapString(text))));
|
|
230
|
+
else {
|
|
231
|
+
const code = generateCode(source).code;
|
|
232
|
+
const expr = parseExpression(`({ ${code} })`);
|
|
233
|
+
parent.properties.push(expr.$ast.properties[0]);
|
|
234
|
+
}
|
|
235
|
+
return true;
|
|
236
|
+
}
|
|
237
|
+
/** Leaf paths of a runtime dictionary value (plain objects recurse; everything else is a leaf). */
|
|
238
|
+
function flattenPaths(value, prefix = []) {
|
|
239
|
+
if (value && typeof value === "object" && !Array.isArray(value)) return Object.entries(value).flatMap(([k, v]) => flattenPaths(v, [...prefix, k]));
|
|
240
|
+
return prefix.length > 0 ? [prefix] : [];
|
|
241
|
+
}
|
|
242
|
+
const toRange = (loc) => loc ? {
|
|
243
|
+
start: {
|
|
244
|
+
line: loc.start.line - 1,
|
|
245
|
+
column: loc.start.column
|
|
246
|
+
},
|
|
247
|
+
end: {
|
|
248
|
+
line: loc.end.line - 1,
|
|
249
|
+
column: loc.end.column
|
|
250
|
+
}
|
|
251
|
+
} : void 0;
|
|
252
|
+
/**
|
|
253
|
+
* Where a locale (no `path`) or a key within it is written. The range covers the property key, so
|
|
254
|
+
* it is a good place to attach a diagnostic or jump to.
|
|
255
|
+
*/
|
|
256
|
+
function locationOf(df, locale, path = []) {
|
|
257
|
+
const prop = path.length === 0 ? findProp(df.localesObj, locale) : getProp(df, locale, path);
|
|
258
|
+
return toRange((prop?.key ?? prop)?.loc);
|
|
259
|
+
}
|
|
260
|
+
/** The source text of the dictionary object itself (the object whose keys are locales). */
|
|
261
|
+
function dictionaryLocation(df) {
|
|
262
|
+
return toRange(df.localesObj.loc);
|
|
263
|
+
}
|
|
264
|
+
/** The source code of a leaf's value (a function's source, or a string with its quotes). */
|
|
265
|
+
function getLeafSource(df, locale, path) {
|
|
266
|
+
const prop = getProp(df, locale, path);
|
|
267
|
+
if (!prop) return void 0;
|
|
268
|
+
return generateCode(prop.type === "ObjectMethod" ? prop : prop.value).code;
|
|
269
|
+
}
|
|
270
|
+
//#endregion
|
|
271
|
+
//#region src/utils/icu-check.ts
|
|
272
|
+
/** CLDR order. `resolvedOptions().pluralCategories` order differs between Node versions. */
|
|
273
|
+
const CATEGORY_ORDER = [
|
|
274
|
+
"zero",
|
|
275
|
+
"one",
|
|
276
|
+
"two",
|
|
277
|
+
"few",
|
|
278
|
+
"many"
|
|
279
|
+
];
|
|
280
|
+
function requiredCategories(locale, ordinal) {
|
|
281
|
+
try {
|
|
282
|
+
return new Intl.PluralRules(locale, { type: ordinal ? "ordinal" : "cardinal" }).resolvedOptions().pluralCategories.filter((c) => c !== "other").sort((a, b) => CATEGORY_ORDER.indexOf(a) - CATEGORY_ORDER.indexOf(b));
|
|
283
|
+
} catch {
|
|
284
|
+
return [];
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* Validates ICU messages across the locales of one dictionary:
|
|
289
|
+
*
|
|
290
|
+
* - invalid syntax: an error for messages that clearly use ICU (plural/select/number/date/time),
|
|
291
|
+
* a warning for plain `{placeholders}` that do not parse (they may not be meant as ICU);
|
|
292
|
+
* - every locale must use the same arguments as the reference locale;
|
|
293
|
+
* - plural options should cover the categories the locale's plural rules need.
|
|
294
|
+
*/
|
|
295
|
+
function checkIcu(dictionary, strings, defaultLocale) {
|
|
296
|
+
const findings = [];
|
|
297
|
+
const parsed = /* @__PURE__ */ new Map();
|
|
298
|
+
for (const [locale, entries] of strings) {
|
|
299
|
+
const ok = /* @__PURE__ */ new Map();
|
|
300
|
+
for (const [path, value] of entries) {
|
|
301
|
+
if (!value.includes("{")) continue;
|
|
302
|
+
const key = `${dictionary}.${path}`;
|
|
303
|
+
try {
|
|
304
|
+
ok.set(path, parseMessage(value));
|
|
305
|
+
} catch (error) {
|
|
306
|
+
const structured = isIcuStructured(value);
|
|
307
|
+
findings.push({
|
|
308
|
+
level: structured ? "error" : "warning",
|
|
309
|
+
code: "invalid-icu",
|
|
310
|
+
locale,
|
|
311
|
+
key,
|
|
312
|
+
message: structured ? `Invalid ICU message "${key}" (${locale}): ${error.message}` : `"${key}" (${locale}) contains braces but is not valid ICU, so t() values cannot be applied: ${error.message}`
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
parsed.set(locale, ok);
|
|
317
|
+
}
|
|
318
|
+
const order = [defaultLocale, ...[...parsed.keys()].filter((l) => l !== defaultLocale)];
|
|
319
|
+
const paths = new Set([...parsed.values()].flatMap((m) => [...m.keys()]));
|
|
320
|
+
for (const path of paths) {
|
|
321
|
+
const key = `${dictionary}.${path}`;
|
|
322
|
+
const reference = order.find((l) => parsed.get(l)?.has(path));
|
|
323
|
+
if (!reference) continue;
|
|
324
|
+
const expected = argumentSignature(parsed.get(reference).get(path));
|
|
325
|
+
for (const locale of order) {
|
|
326
|
+
const nodes = parsed.get(locale)?.get(path);
|
|
327
|
+
if (nodes && locale !== reference) {
|
|
328
|
+
const found = argumentSignature(nodes);
|
|
329
|
+
if (found !== expected) findings.push({
|
|
330
|
+
level: "error",
|
|
331
|
+
code: "icu-args-mismatch",
|
|
332
|
+
locale,
|
|
333
|
+
key,
|
|
334
|
+
message: `Locale "${locale}" uses different arguments than "${reference}" for "${key}" (expected ${expected || "none"}, found ${found || "none"})`
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
if (!nodes) continue;
|
|
338
|
+
for (const plural of findPlurals(nodes)) {
|
|
339
|
+
const missing = requiredCategories(locale, plural.ordinal).filter((c) => !(c in plural.options));
|
|
340
|
+
if (missing.length > 0) findings.push({
|
|
341
|
+
level: "warning",
|
|
342
|
+
code: "icu-plural-categories",
|
|
343
|
+
locale,
|
|
344
|
+
key,
|
|
345
|
+
message: `Locale "${locale}" plural "${plural.name}" in "${key}" has no ${missing.map((c) => `"${c}"`).join(", ")} option (used by this language; "other" will be shown instead)`
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
return findings;
|
|
351
|
+
}
|
|
352
|
+
//#endregion
|
|
353
|
+
//#region src/analysis.ts
|
|
354
|
+
const DICTIONARY_IGNORE = [
|
|
355
|
+
"**/node_modules/**",
|
|
356
|
+
"**/dist/**",
|
|
357
|
+
"**/.next/**"
|
|
358
|
+
];
|
|
359
|
+
const DEFAULT_SOURCE_GLOB = "**/*.{ts,tsx,js,jsx,mjs,cjs,vue,svelte,astro,mdx}";
|
|
360
|
+
const SOURCE_IGNORE = [
|
|
361
|
+
...DICTIONARY_IGNORE,
|
|
362
|
+
"**/.nuxt/**",
|
|
363
|
+
"**/.output/**",
|
|
364
|
+
"**/.svelte-kit/**",
|
|
365
|
+
"**/.astro/**",
|
|
366
|
+
"**/coverage/**",
|
|
367
|
+
"**/graphify-out/**",
|
|
368
|
+
"**/*.d.ts",
|
|
369
|
+
"**/i18n.generated.*",
|
|
370
|
+
"**/dialex.config.*",
|
|
371
|
+
"**/i18n.config.*"
|
|
372
|
+
];
|
|
373
|
+
/** `getDictionary("x")`, `useDictionary("x")`, `@I18nDictionary("x")` */
|
|
374
|
+
const DICTIONARY_CALL = /\b(?:getDictionary|useDictionary|I18nDictionary)\(\s*(["'`])([^"'`$\n]+)\1/dg;
|
|
375
|
+
/** `t("dictionary.path.to.key")` */
|
|
376
|
+
const T_CALL = /(?<![\w$.])t\(\s*(["'`])([\w-]+(?:\.[\w-]+)+)\1/dg;
|
|
377
|
+
/** Finds `getDictionary("x")`-style and `t("x.y.z")` references in source text. */
|
|
378
|
+
function scanReferences(text) {
|
|
379
|
+
const references = [];
|
|
380
|
+
const collect = (pattern, kind) => {
|
|
381
|
+
for (const match of text.matchAll(pattern)) {
|
|
382
|
+
const [start, end] = match.indices[2];
|
|
383
|
+
references.push({
|
|
384
|
+
kind,
|
|
385
|
+
value: match[2],
|
|
386
|
+
start,
|
|
387
|
+
end
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
};
|
|
391
|
+
collect(DICTIONARY_CALL, "dictionary");
|
|
392
|
+
collect(T_CALL, "path");
|
|
393
|
+
return references.sort((a, b) => a.start - b.start);
|
|
394
|
+
}
|
|
395
|
+
function lineStarts(text) {
|
|
396
|
+
const starts = [0];
|
|
397
|
+
for (let i = 0; i < text.length; i++) if (text.charCodeAt(i) === 10) starts.push(i + 1);
|
|
398
|
+
return starts;
|
|
399
|
+
}
|
|
400
|
+
function positionAt(starts, offset) {
|
|
401
|
+
let low = 0;
|
|
402
|
+
let high = starts.length - 1;
|
|
403
|
+
while (low < high) {
|
|
404
|
+
const mid = Math.ceil((low + high) / 2);
|
|
405
|
+
if (starts[mid] <= offset) low = mid;
|
|
406
|
+
else high = mid - 1;
|
|
407
|
+
}
|
|
408
|
+
return {
|
|
409
|
+
line: low,
|
|
410
|
+
column: offset - starts[low]
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
function fromSyntaxTree(df) {
|
|
414
|
+
const locales = listLocales(df);
|
|
415
|
+
const leaves = /* @__PURE__ */ new Map();
|
|
416
|
+
for (const locale of locales) {
|
|
417
|
+
const entries = /* @__PURE__ */ new Map();
|
|
418
|
+
for (const leaf of listLeaves(df, locale)) entries.set(leaf.path.join("."), {
|
|
419
|
+
kind: leaf.kind,
|
|
420
|
+
value: leaf.value
|
|
421
|
+
});
|
|
422
|
+
leaves.set(locale, entries);
|
|
423
|
+
}
|
|
424
|
+
return {
|
|
425
|
+
name: df.name,
|
|
426
|
+
locales,
|
|
427
|
+
leaves,
|
|
428
|
+
df
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
async function fromRuntime(file) {
|
|
432
|
+
try {
|
|
433
|
+
const mod = await import(`${pathToFileURL(file).href}?t=${Date.now()}_${Math.random()}`);
|
|
434
|
+
const definition = mod.default || mod;
|
|
435
|
+
if (!definition || typeof definition !== "object" || !definition.dictionary) return {
|
|
436
|
+
ok: false,
|
|
437
|
+
level: "error",
|
|
438
|
+
code: "invalid-dictionary",
|
|
439
|
+
message: "Default export does not appear to be a valid defineDictionary output."
|
|
440
|
+
};
|
|
441
|
+
const dictionary = definition.dictionary;
|
|
442
|
+
const leaves = /* @__PURE__ */ new Map();
|
|
443
|
+
const invalid = [];
|
|
444
|
+
for (const [locale, content] of Object.entries(dictionary)) {
|
|
445
|
+
if (!content || typeof content !== "object") {
|
|
446
|
+
invalid.push(locale);
|
|
447
|
+
continue;
|
|
448
|
+
}
|
|
449
|
+
const entries = /* @__PURE__ */ new Map();
|
|
450
|
+
for (const leafPath of flattenPaths(content)) {
|
|
451
|
+
let value = content;
|
|
452
|
+
for (const segment of leafPath) value = value?.[segment];
|
|
453
|
+
entries.set(leafPath.join("."), typeof value === "string" ? {
|
|
454
|
+
kind: "string",
|
|
455
|
+
value
|
|
456
|
+
} : { kind: typeof value === "function" ? "function" : "other" });
|
|
457
|
+
}
|
|
458
|
+
leaves.set(locale, entries);
|
|
459
|
+
}
|
|
460
|
+
if (invalid.length > 0) return {
|
|
461
|
+
ok: false,
|
|
462
|
+
level: "error",
|
|
463
|
+
code: "invalid-locale",
|
|
464
|
+
message: invalid.map((l) => `Locale "${l}" content is empty or invalid.`).join("\n")
|
|
465
|
+
};
|
|
466
|
+
return {
|
|
467
|
+
ok: true,
|
|
468
|
+
data: {
|
|
469
|
+
name: String(definition.name ?? ""),
|
|
470
|
+
locales: [...leaves.keys()],
|
|
471
|
+
leaves
|
|
472
|
+
}
|
|
473
|
+
};
|
|
474
|
+
} catch (error) {
|
|
475
|
+
return {
|
|
476
|
+
ok: false,
|
|
477
|
+
level: "error",
|
|
478
|
+
code: "invalid-dictionary",
|
|
479
|
+
message: `Failed to import dictionary: ${error.message || String(error)}`
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
async function loadDictionaryData(file, runtime) {
|
|
484
|
+
let astError;
|
|
485
|
+
try {
|
|
486
|
+
const df = await loadDictionaryFile(file);
|
|
487
|
+
if (df?.complete && df.name) return {
|
|
488
|
+
ok: true,
|
|
489
|
+
data: fromSyntaxTree(df)
|
|
490
|
+
};
|
|
491
|
+
} catch (error) {
|
|
492
|
+
astError = error;
|
|
493
|
+
}
|
|
494
|
+
if (runtime) return fromRuntime(file);
|
|
495
|
+
return astError ? {
|
|
496
|
+
ok: false,
|
|
497
|
+
level: "error",
|
|
498
|
+
code: "invalid-dictionary",
|
|
499
|
+
message: `Failed to parse dictionary: ${astError.message}`
|
|
500
|
+
} : {
|
|
501
|
+
ok: false,
|
|
502
|
+
level: "info",
|
|
503
|
+
code: "unanalyzable",
|
|
504
|
+
message: "This dictionary uses spreads, computed keys, imported values, or a computed name, so it cannot be analyzed statically."
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
async function analyzeProject(options) {
|
|
508
|
+
const { root, config } = options;
|
|
509
|
+
const include = config.include || "**/*.content.ts";
|
|
510
|
+
const configuredLocales = config.locales || [];
|
|
511
|
+
const defaultLocale = config.defaultLocale || "en";
|
|
512
|
+
const extraIgnore = options.ignore ?? [];
|
|
513
|
+
const files = fg.sync(include, {
|
|
514
|
+
cwd: root,
|
|
515
|
+
absolute: true,
|
|
516
|
+
ignore: [...DICTIONARY_IGNORE, ...extraIgnore]
|
|
517
|
+
});
|
|
518
|
+
const issues = [];
|
|
519
|
+
const dictionaries = [];
|
|
520
|
+
for (const file of files) {
|
|
521
|
+
const loaded = await loadDictionaryData(file, options.runtime === true);
|
|
522
|
+
if (!loaded.ok) {
|
|
523
|
+
for (const message of loaded.message.split("\n")) issues.push({
|
|
524
|
+
file,
|
|
525
|
+
level: loaded.level,
|
|
526
|
+
code: loaded.code,
|
|
527
|
+
message
|
|
528
|
+
});
|
|
529
|
+
continue;
|
|
530
|
+
}
|
|
531
|
+
const { data } = loaded;
|
|
532
|
+
const { df } = data;
|
|
533
|
+
const at = (locale, keyPath) => df && locale ? locationOf(df, locale, keyPath) : void 0;
|
|
534
|
+
const add = (issue) => issues.push({
|
|
535
|
+
...issue,
|
|
536
|
+
file,
|
|
537
|
+
dictionary: data.name
|
|
538
|
+
});
|
|
539
|
+
for (const required of configuredLocales) if (!data.locales.includes(required)) add({
|
|
540
|
+
level: "error",
|
|
541
|
+
code: "missing-locale",
|
|
542
|
+
message: `Missing configured locale "${required}"`,
|
|
543
|
+
range: df ? dictionaryLocation(df) : void 0,
|
|
544
|
+
locale: required
|
|
545
|
+
});
|
|
546
|
+
const paths = [];
|
|
547
|
+
const seen = /* @__PURE__ */ new Set();
|
|
548
|
+
for (const locale of data.locales) for (const leafPath of data.leaves.get(locale).keys()) if (!seen.has(leafPath)) {
|
|
549
|
+
seen.add(leafPath);
|
|
550
|
+
paths.push(leafPath);
|
|
551
|
+
}
|
|
552
|
+
for (const locale of data.locales) {
|
|
553
|
+
const present = data.leaves.get(locale);
|
|
554
|
+
for (const leafPath of paths) {
|
|
555
|
+
if (present.has(leafPath)) continue;
|
|
556
|
+
const sourceLocale = [defaultLocale, ...data.locales].find((l) => l !== locale && data.leaves.get(l)?.has(leafPath));
|
|
557
|
+
add({
|
|
558
|
+
level: "error",
|
|
559
|
+
code: "missing-key",
|
|
560
|
+
message: `Locale "${locale}" is missing key "${leafPath}"`,
|
|
561
|
+
range: at(locale),
|
|
562
|
+
locale,
|
|
563
|
+
path: leafPath.split("."),
|
|
564
|
+
sourceLocale
|
|
565
|
+
});
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
const strings = /* @__PURE__ */ new Map();
|
|
569
|
+
for (const locale of data.locales) {
|
|
570
|
+
const entries = /* @__PURE__ */ new Map();
|
|
571
|
+
strings.set(locale, entries);
|
|
572
|
+
for (const [leafPath, leaf] of data.leaves.get(locale)) {
|
|
573
|
+
if (leaf.kind !== "string" || leaf.value === void 0) continue;
|
|
574
|
+
entries.set(leafPath, leaf.value);
|
|
575
|
+
if (leaf.value.startsWith("[TODO] ")) add({
|
|
576
|
+
level: "warning",
|
|
577
|
+
code: "todo-placeholder",
|
|
578
|
+
message: `Locale "${locale}" key "${leafPath}" still has a ${TODO_PREFIX.trim()} placeholder`,
|
|
579
|
+
range: at(locale, leafPath.split(".")),
|
|
580
|
+
locale,
|
|
581
|
+
path: leafPath.split(".")
|
|
582
|
+
});
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
for (const finding of checkIcu(data.name, strings, defaultLocale)) {
|
|
586
|
+
const keyPath = finding.key.slice(data.name.length + 1).split(".");
|
|
587
|
+
add({
|
|
588
|
+
level: finding.level,
|
|
589
|
+
code: finding.code,
|
|
590
|
+
message: finding.message,
|
|
591
|
+
range: at(finding.locale, keyPath),
|
|
592
|
+
locale: finding.locale,
|
|
593
|
+
path: keyPath
|
|
594
|
+
});
|
|
595
|
+
}
|
|
596
|
+
dictionaries.push({
|
|
597
|
+
name: data.name,
|
|
598
|
+
file,
|
|
599
|
+
locales: data.locales,
|
|
600
|
+
leaves: data.leaves,
|
|
601
|
+
paths,
|
|
602
|
+
df
|
|
603
|
+
});
|
|
604
|
+
}
|
|
605
|
+
const known = new Map(dictionaries.map((d) => [d.name, d]));
|
|
606
|
+
const dictionaryFiles = new Set(files);
|
|
607
|
+
const sourceGlobs = options.src ?? ["**/*.{ts,tsx,js,jsx,mjs,cjs,vue,svelte,astro,mdx}"];
|
|
608
|
+
const sourceFiles = sourceGlobs.length === 0 ? [] : fg.sync(sourceGlobs, {
|
|
609
|
+
cwd: root,
|
|
610
|
+
absolute: true,
|
|
611
|
+
ignore: [...SOURCE_IGNORE, ...extraIgnore]
|
|
612
|
+
}).filter((f) => !dictionaryFiles.has(f));
|
|
613
|
+
if (sourceFiles.length > 0) {
|
|
614
|
+
const tokens = /* @__PURE__ */ new Set();
|
|
615
|
+
for (const file of sourceFiles) {
|
|
616
|
+
let text;
|
|
617
|
+
try {
|
|
618
|
+
text = fs.readFileSync(file, "utf-8");
|
|
619
|
+
} catch {
|
|
620
|
+
continue;
|
|
621
|
+
}
|
|
622
|
+
for (const token of text.match(/[\w$-]+/g) ?? []) tokens.add(token);
|
|
623
|
+
const starts = lineStarts(text);
|
|
624
|
+
const rangeOf = (start, end) => ({
|
|
625
|
+
start: positionAt(starts, start),
|
|
626
|
+
end: positionAt(starts, end)
|
|
627
|
+
});
|
|
628
|
+
for (const ref of scanReferences(text)) {
|
|
629
|
+
if (ref.kind === "dictionary") {
|
|
630
|
+
if (!known.has(ref.value)) issues.push({
|
|
631
|
+
file,
|
|
632
|
+
level: "error",
|
|
633
|
+
code: "unknown-dictionary",
|
|
634
|
+
message: `Unknown dictionary "${ref.value}"`,
|
|
635
|
+
range: rangeOf(ref.start, ref.end),
|
|
636
|
+
dictionary: ref.value
|
|
637
|
+
});
|
|
638
|
+
continue;
|
|
639
|
+
}
|
|
640
|
+
const [name, ...rest] = ref.value.split(".");
|
|
641
|
+
const dictionary = known.get(name);
|
|
642
|
+
if (!dictionary) continue;
|
|
643
|
+
const keyPath = rest.join(".");
|
|
644
|
+
if (!dictionary.paths.some((p) => p === keyPath || p.startsWith(`${keyPath}.`))) issues.push({
|
|
645
|
+
file,
|
|
646
|
+
level: "error",
|
|
647
|
+
code: "unknown-path",
|
|
648
|
+
message: `Unknown translation path "${ref.value}"`,
|
|
649
|
+
range: rangeOf(ref.start, ref.end),
|
|
650
|
+
dictionary: name,
|
|
651
|
+
path: rest
|
|
652
|
+
});
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
if (options.unused !== false) for (const dictionary of dictionaries) {
|
|
656
|
+
if (!tokens.has(dictionary.name)) {
|
|
657
|
+
issues.push({
|
|
658
|
+
file: dictionary.file,
|
|
659
|
+
level: "warning",
|
|
660
|
+
code: "unused-dictionary",
|
|
661
|
+
message: `Dictionary "${dictionary.name}" is never referenced`,
|
|
662
|
+
range: dictionary.df ? dictionaryLocation(dictionary.df) : void 0,
|
|
663
|
+
dictionary: dictionary.name
|
|
664
|
+
});
|
|
665
|
+
continue;
|
|
666
|
+
}
|
|
667
|
+
for (const keyPath of dictionary.paths) {
|
|
668
|
+
const last = keyPath.split(".").pop();
|
|
669
|
+
if (tokens.has(last)) continue;
|
|
670
|
+
const holder = dictionary.locales.find((l) => dictionary.leaves.get(l)?.has(keyPath));
|
|
671
|
+
issues.push({
|
|
672
|
+
file: dictionary.file,
|
|
673
|
+
level: "warning",
|
|
674
|
+
code: "unused-key",
|
|
675
|
+
message: `Key "${keyPath}" is possibly unused`,
|
|
676
|
+
range: dictionary.df && holder ? locationOf(dictionary.df, holder, keyPath.split(".")) : void 0,
|
|
677
|
+
dictionary: dictionary.name,
|
|
678
|
+
path: keyPath.split(".")
|
|
679
|
+
});
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
return {
|
|
684
|
+
issues,
|
|
685
|
+
dictionaries,
|
|
686
|
+
files,
|
|
687
|
+
sourceFilesScanned: sourceFiles.length
|
|
688
|
+
};
|
|
689
|
+
}
|
|
690
|
+
/** Project root-relative path with forward slashes, for display. */
|
|
691
|
+
function relativeFile(root, file) {
|
|
692
|
+
return path.relative(root, file);
|
|
693
|
+
}
|
|
694
|
+
//#endregion
|
|
695
|
+
//#region src/utils/static-config.ts
|
|
696
|
+
const CONFIG_NAMES = ["dialex.config", "i18n.config"];
|
|
697
|
+
const EXTENSIONS = [
|
|
698
|
+
"ts",
|
|
699
|
+
"mts",
|
|
700
|
+
"cts",
|
|
701
|
+
"js",
|
|
702
|
+
"mjs",
|
|
703
|
+
"cjs",
|
|
704
|
+
"json"
|
|
705
|
+
];
|
|
706
|
+
const DEFAULT_STATIC_CONFIG = {
|
|
707
|
+
defaultLocale: "en",
|
|
708
|
+
locales: [],
|
|
709
|
+
include: "**/*.content.ts",
|
|
710
|
+
fallbacks: {},
|
|
711
|
+
prefixDefault: true,
|
|
712
|
+
lazy: false
|
|
713
|
+
};
|
|
714
|
+
const NOT_LITERAL = Symbol("not literal");
|
|
715
|
+
function literal(node) {
|
|
716
|
+
node = unwrap(node);
|
|
717
|
+
switch (node?.type) {
|
|
718
|
+
case "StringLiteral":
|
|
719
|
+
case "NumericLiteral":
|
|
720
|
+
case "BooleanLiteral": return node.value;
|
|
721
|
+
case "Literal": return node.value;
|
|
722
|
+
case "TemplateLiteral": return node.expressions.length === 0 ? node.quasis[0]?.value?.cooked ?? "" : NOT_LITERAL;
|
|
723
|
+
case "ArrayExpression": {
|
|
724
|
+
const items = node.elements.map((e) => literal(e));
|
|
725
|
+
return items.includes(NOT_LITERAL) ? NOT_LITERAL : items;
|
|
726
|
+
}
|
|
727
|
+
case "ObjectExpression": {
|
|
728
|
+
const result = {};
|
|
729
|
+
for (const prop of node.properties) {
|
|
730
|
+
const key = keyName(prop);
|
|
731
|
+
const value = key === void 0 ? NOT_LITERAL : literal(prop.value);
|
|
732
|
+
if (value === NOT_LITERAL) return NOT_LITERAL;
|
|
733
|
+
result[key] = value;
|
|
734
|
+
}
|
|
735
|
+
return result;
|
|
736
|
+
}
|
|
737
|
+
default: return NOT_LITERAL;
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
/** The object literal passed to `defineConfig({...})`, or exported directly. */
|
|
741
|
+
function findConfigObject(declaration) {
|
|
742
|
+
const node = unwrap(declaration);
|
|
743
|
+
if (node?.type === "CallExpression") return findConfigObject(node.arguments[0]);
|
|
744
|
+
return isObject(node) ? node : void 0;
|
|
745
|
+
}
|
|
746
|
+
const isStringArray = (value) => Array.isArray(value) && value.every((v) => typeof v === "string");
|
|
747
|
+
function apply(object, config, notes) {
|
|
748
|
+
const accept = (key, valid, expected) => {
|
|
749
|
+
if (!(key in object)) return;
|
|
750
|
+
const value = object[key];
|
|
751
|
+
if (valid(value)) config[key] = value;
|
|
752
|
+
else notes.push(`"${key}" could not be read statically (expected ${expected}).`);
|
|
753
|
+
};
|
|
754
|
+
accept("defaultLocale", (v) => typeof v === "string" && v !== "", "a string literal");
|
|
755
|
+
accept("locales", isStringArray, "an array of string literals");
|
|
756
|
+
accept("include", (v) => typeof v === "string" || isStringArray(v), "a string or array of strings");
|
|
757
|
+
accept("fallbacks", (v) => typeof v === "object" && v !== null && Object.values(v).every((list) => isStringArray(list)), "an object of string arrays");
|
|
758
|
+
accept("prefixDefault", (v) => typeof v === "boolean", "true or false");
|
|
759
|
+
accept("lazy", (v) => typeof v === "boolean", "true or false");
|
|
760
|
+
}
|
|
761
|
+
/** Reads a config file's source text. Exposed for tests and for unsaved buffers. */
|
|
762
|
+
function parseStaticConfig(text, file = "dialex.config.ts") {
|
|
763
|
+
const notes = [];
|
|
764
|
+
const config = { ...DEFAULT_STATIC_CONFIG };
|
|
765
|
+
if (file.endsWith(".json")) {
|
|
766
|
+
try {
|
|
767
|
+
const data = JSON.parse(text);
|
|
768
|
+
if (data && typeof data === "object") apply(data, config, notes);
|
|
769
|
+
} catch (error) {
|
|
770
|
+
notes.push(`Could not parse ${path.basename(file)}: ${error.message}`);
|
|
771
|
+
}
|
|
772
|
+
return {
|
|
773
|
+
config,
|
|
774
|
+
file,
|
|
775
|
+
notes
|
|
776
|
+
};
|
|
777
|
+
}
|
|
778
|
+
try {
|
|
779
|
+
const exported = parseModule(text).exports.default;
|
|
780
|
+
const object = findConfigObject(exported?.$ast?.declaration ?? exported?.$ast);
|
|
781
|
+
if (!object) notes.push(`${path.basename(file)} does not export a config object literal.`);
|
|
782
|
+
else {
|
|
783
|
+
const values = {};
|
|
784
|
+
for (const key of [
|
|
785
|
+
"defaultLocale",
|
|
786
|
+
"locales",
|
|
787
|
+
"include",
|
|
788
|
+
"fallbacks",
|
|
789
|
+
"prefixDefault",
|
|
790
|
+
"lazy"
|
|
791
|
+
]) {
|
|
792
|
+
const prop = findProp(object, key);
|
|
793
|
+
if (!prop) continue;
|
|
794
|
+
const value = literal(prop.value);
|
|
795
|
+
if (value === NOT_LITERAL) notes.push(`"${key}" could not be read statically (not a literal).`);
|
|
796
|
+
else values[key] = value;
|
|
797
|
+
}
|
|
798
|
+
apply(values, config, notes);
|
|
799
|
+
}
|
|
800
|
+
} catch (error) {
|
|
801
|
+
notes.push(`Could not parse ${path.basename(file)}: ${error.message}`);
|
|
802
|
+
}
|
|
803
|
+
return {
|
|
804
|
+
config,
|
|
805
|
+
file,
|
|
806
|
+
notes
|
|
807
|
+
};
|
|
808
|
+
}
|
|
809
|
+
/** Reads `dialex.config.*` (or `i18n.config.*`) from `root`, or from `configFile` if given. */
|
|
810
|
+
function readStaticConfig(root, configFile) {
|
|
811
|
+
const candidates = configFile ? [path.resolve(root, configFile)] : CONFIG_NAMES.flatMap((name) => EXTENSIONS.map((ext) => path.join(root, `${name}.${ext}`)));
|
|
812
|
+
for (const file of candidates) if (fs.existsSync(file)) return parseStaticConfig(fs.readFileSync(file, "utf-8"), file);
|
|
813
|
+
return {
|
|
814
|
+
config: { ...DEFAULT_STATIC_CONFIG },
|
|
815
|
+
notes: []
|
|
816
|
+
};
|
|
817
|
+
}
|
|
818
|
+
//#endregion
|
|
819
|
+
//#region src/utils/project.ts
|
|
820
|
+
async function loadDictionaries(root, config) {
|
|
821
|
+
const include = config.include || "**/*.content.ts";
|
|
822
|
+
const files = fg.sync(include, {
|
|
823
|
+
cwd: root,
|
|
824
|
+
absolute: true,
|
|
825
|
+
ignore: [
|
|
826
|
+
"**/node_modules/**",
|
|
827
|
+
"**/dist/**",
|
|
828
|
+
"**/.next/**"
|
|
829
|
+
]
|
|
830
|
+
});
|
|
831
|
+
const dictionaries = [];
|
|
832
|
+
const unsupported = [];
|
|
833
|
+
for (const file of files) {
|
|
834
|
+
const rel = path.relative(root, file);
|
|
835
|
+
try {
|
|
836
|
+
const df = await loadDictionaryFile(file);
|
|
837
|
+
if (!df || !df.name) unsupported.push(rel);
|
|
838
|
+
else dictionaries.push({
|
|
839
|
+
rel,
|
|
840
|
+
name: df.name,
|
|
841
|
+
df
|
|
842
|
+
});
|
|
843
|
+
} catch {
|
|
844
|
+
unsupported.push(rel);
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
return {
|
|
848
|
+
dictionaries,
|
|
849
|
+
unsupported
|
|
850
|
+
};
|
|
851
|
+
}
|
|
852
|
+
/**
|
|
853
|
+
* Resolves the config (running `dialex.config.*` through the loader, which executes it) and loads
|
|
854
|
+
* every dictionary file for AST-level reading and editing.
|
|
855
|
+
*/
|
|
856
|
+
async function loadProject(root, configFile) {
|
|
857
|
+
const config = resolveI18nConfig(root, configFile ? { configFile } : {});
|
|
858
|
+
return {
|
|
859
|
+
root,
|
|
860
|
+
config,
|
|
861
|
+
notes: [],
|
|
862
|
+
...await loadDictionaries(root, config)
|
|
863
|
+
};
|
|
864
|
+
}
|
|
865
|
+
/**
|
|
866
|
+
* Like `loadProject`, but reads the config from the syntax tree without executing it. Use this in
|
|
867
|
+
* editors and anywhere project code must not run.
|
|
868
|
+
*/
|
|
869
|
+
async function loadStaticProject(root, configFile) {
|
|
870
|
+
const { config, notes } = readStaticConfig(root, configFile);
|
|
871
|
+
return {
|
|
872
|
+
root,
|
|
873
|
+
config,
|
|
874
|
+
notes,
|
|
875
|
+
...await loadDictionaries(root, config)
|
|
876
|
+
};
|
|
877
|
+
}
|
|
878
|
+
/** `home` + `["nav", "about"]` -> `home.nav.about` */
|
|
879
|
+
const toKey = (dictionary, keyPath) => [dictionary, ...keyPath].join(".");
|
|
880
|
+
//#endregion
|
|
881
|
+
export { saveDictionaryFile as C, renderDictionaryFile as S, listLeaves as _, parseStaticConfig as a, locationOf as b, analyzeProject as c, TODO_PREFIX as d, copyLeaf as f, hasPath as g, getString as h, DEFAULT_STATIC_CONFIG as i, relativeFile as l, getLeafSource as m, loadStaticProject as n, readStaticConfig as o, dictionaryLocation as p, toKey as r, DEFAULT_SOURCE_GLOB as s, loadProject as t, scanReferences as u, listLocales as v, setString as w, parseDictionaryText as x, loadDictionaryFile as y };
|
|
882
|
+
|
|
883
|
+
//# sourceMappingURL=project-D4PmfqxM.mjs.map
|