@x18n/core 0.1.6 → 0.1.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +366 -241
- package/dist/index.mjs +404 -107
- package/package.json +1 -1
- package/schema/config.json +49 -1
- package/schema/plugin.json +34 -1
package/dist/index.mjs
CHANGED
|
@@ -286,26 +286,51 @@ function fuzzyScore(query, text) {
|
|
|
286
286
|
}
|
|
287
287
|
//#endregion
|
|
288
288
|
//#region src/keys.ts
|
|
289
|
-
/**
|
|
290
|
-
function
|
|
291
|
-
|
|
292
|
-
return namespace === void 0 ? dotted : `${namespace}:${dotted}`;
|
|
289
|
+
/** The text between path segments in a key string: the separator, or `.` when keys are literal. */
|
|
290
|
+
function keyJoiner(separator) {
|
|
291
|
+
return separator === false ? "." : separator;
|
|
293
292
|
}
|
|
294
|
-
/**
|
|
295
|
-
function
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
293
|
+
/** Path segments of the part of a key after its namespace. With `false` the whole text is one segment. */
|
|
294
|
+
function splitKeyPath(text, separator = ".") {
|
|
295
|
+
if (text === "") return [];
|
|
296
|
+
return separator === false ? [text] : text.split(separator);
|
|
297
|
+
}
|
|
298
|
+
/** Canonical string form of a key: `ns:a.b.c`, or `a.b.c` without namespace. `separator` joins the path (`.` by default). */
|
|
299
|
+
function joinKey(namespace, path, separator = ".") {
|
|
300
|
+
const joined = path.join(keyJoiner(separator));
|
|
301
|
+
return namespace === void 0 ? joined : `${namespace}:${joined}`;
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* Splits a key string. `namespaced` is false for projects without namespaces, where `:` is part of the key.
|
|
305
|
+
* The namespace ends at the first `:`, so the rest of the key may hold more. With `separator: false` the path is one literal segment.
|
|
306
|
+
*/
|
|
307
|
+
function splitKey(key, namespaced = true, separator = ".") {
|
|
308
|
+
const { namespace, rest } = splitNamespace(key, namespaced);
|
|
299
309
|
return {
|
|
300
310
|
namespace,
|
|
301
|
-
path: rest
|
|
311
|
+
path: splitKeyPath(rest, separator)
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
/** The namespace of a key string and the text after it. */
|
|
315
|
+
function splitNamespace(key, namespaced) {
|
|
316
|
+
const index = namespaced ? key.indexOf(":") : -1;
|
|
317
|
+
return index === -1 ? {
|
|
318
|
+
namespace: void 0,
|
|
319
|
+
rest: key
|
|
320
|
+
} : {
|
|
321
|
+
namespace: key.slice(0, index),
|
|
322
|
+
rest: key.slice(index + 1)
|
|
302
323
|
};
|
|
303
324
|
}
|
|
304
|
-
/**
|
|
305
|
-
function
|
|
325
|
+
/** The key separator of a resource: its own (from its resource spec), else the config's. */
|
|
326
|
+
function keySeparatorOf(resource, fallback) {
|
|
327
|
+
return resource.keySeparator ?? fallback;
|
|
328
|
+
}
|
|
329
|
+
/** True when `key` is `prefix` itself or lies under it (`a.b` is under `a`). A literal key (`separator: false`) has nothing under it but in its namespace. */
|
|
330
|
+
function isKeyUnder(key, prefix, separator = ".") {
|
|
306
331
|
if (key === prefix) return true;
|
|
307
|
-
|
|
308
|
-
return key.startsWith(prefix +
|
|
332
|
+
if (prefix.endsWith(":")) return key.startsWith(prefix);
|
|
333
|
+
return separator !== false && key.startsWith(prefix + separator);
|
|
309
334
|
}
|
|
310
335
|
/** Replaces the `from` prefix of `key` with `to`. */
|
|
311
336
|
function replaceKeyPrefix(key, from, to) {
|
|
@@ -354,10 +379,10 @@ function globToRegExp(glob, separator = "/") {
|
|
|
354
379
|
}
|
|
355
380
|
return new RegExp(`^${out}$`);
|
|
356
381
|
}
|
|
357
|
-
/** Matcher over key globs: `*` stays inside a segment, `**` spans segments. */
|
|
358
|
-
function keyMatcher(globs) {
|
|
382
|
+
/** Matcher over key globs: `*` stays inside a segment, `**` spans segments. With literal keys (`separator: false`) `*` spans the whole key. */
|
|
383
|
+
function keyMatcher(globs, separator = ".") {
|
|
359
384
|
if (globs.length === 0) return () => false;
|
|
360
|
-
const regexes = globs.map((glob) => globToRegExp(glob, "
|
|
385
|
+
const regexes = globs.map((glob) => globToRegExp(glob, separator === false ? "\0" : separator));
|
|
361
386
|
return (key) => regexes.some((re) => re.test(key));
|
|
362
387
|
}
|
|
363
388
|
/** Matcher over posix file paths. */
|
|
@@ -381,6 +406,7 @@ function createLibraryPlugin(options) {
|
|
|
381
406
|
};
|
|
382
407
|
if (options.resources) draft.resources = [...draft.resources, ...options.resources.filter((spec) => !draft.resources.some((r) => r.pattern === spec.pattern))];
|
|
383
408
|
if (options.keyStyle && draft.keyStyle === "auto" && !ctx.user.keyStyle) draft.keyStyle = options.keyStyle;
|
|
409
|
+
if (options.keySeparator !== void 0 && ctx.user.keySeparator === void 0) draft.keySeparator = options.keySeparator;
|
|
384
410
|
draft.usage = {
|
|
385
411
|
...draft.usage,
|
|
386
412
|
calls: [...draft.usage.calls, ...options.calls ?? []],
|
|
@@ -666,7 +692,8 @@ function uniqueById(items, kind) {
|
|
|
666
692
|
function createRegistry(input, config) {
|
|
667
693
|
const plugins = sortByEnforce(input);
|
|
668
694
|
const formats = plugins.flatMap((plugin) => [...plugin.formats ?? [], ...plugin.lazyFormats ?? []]);
|
|
669
|
-
const
|
|
695
|
+
const lazyIds = /* @__PURE__ */ new Set();
|
|
696
|
+
const scanners = plugins.flatMap((plugin) => [...plugin.scanners ?? [], ...(plugin.lazyScanners ?? []).filter((entry) => !lazyIds.has(entry.id) && lazyIds.add(entry.id))]);
|
|
670
697
|
const finders = [...uniqueById(plugins.flatMap((plugin) => plugin.localeFinders ?? []), "Locale finder").values()];
|
|
671
698
|
const checks = uniqueById(plugins.flatMap((plugin) => plugin.checks ?? []), "Check");
|
|
672
699
|
const reporters = uniqueById(plugins.flatMap((plugin) => plugin.reporters ?? []), "Reporter");
|
|
@@ -802,6 +829,7 @@ const callSpec = v.strictObject({
|
|
|
802
829
|
keyPrefix: v.optional(argSource),
|
|
803
830
|
params: v.optional(paramsSource),
|
|
804
831
|
defaultValue: v.optional(v.array(argSource)),
|
|
832
|
+
description: v.optional(v.string()),
|
|
805
833
|
factory: v.optional(v.boolean()),
|
|
806
834
|
returns: v.optional(strings),
|
|
807
835
|
types: v.optional(strings)
|
|
@@ -810,12 +838,14 @@ const regexSpec = v.strictObject({
|
|
|
810
838
|
extensions: strings,
|
|
811
839
|
patterns: strings
|
|
812
840
|
});
|
|
841
|
+
const keySeparator = v.union([v.pipe(v.string(), v.minLength(1)), v.literal(false)]);
|
|
813
842
|
const resourceSpec = v.strictObject({
|
|
814
843
|
pattern: v.string(),
|
|
815
844
|
format: v.optional(v.string()),
|
|
816
845
|
namespace: v.optional(v.string()),
|
|
817
846
|
locale: v.optional(v.string()),
|
|
818
|
-
execute: v.optional(v.boolean())
|
|
847
|
+
execute: v.optional(v.boolean()),
|
|
848
|
+
keySeparator: v.optional(keySeparator)
|
|
819
849
|
});
|
|
820
850
|
const pluginSpecifier = v.union([v.string(), v.tuple([v.string(), jsonObject])]);
|
|
821
851
|
const extractTemplate = v.strictObject({
|
|
@@ -852,12 +882,13 @@ const extractOptions = v.strictObject({
|
|
|
852
882
|
const glossaryEntry = v.strictObject({
|
|
853
883
|
term: v.string(),
|
|
854
884
|
translations: v.optional(v.record(v.string(), v.string())),
|
|
885
|
+
forms: v.optional(v.record(v.string(), v.array(v.string()))),
|
|
855
886
|
note: v.optional(v.string()),
|
|
856
887
|
doNotTranslate: v.optional(v.boolean())
|
|
857
888
|
});
|
|
858
889
|
const checkSetting = v.union([level, v.tuple([level, jsonObject])]);
|
|
859
890
|
const namespaceOptions = v.strictObject({
|
|
860
|
-
separator: v.optional(v.string()),
|
|
891
|
+
separator: v.optional(v.union([v.pipe(v.string(), v.minLength(1)), v.literal(false)])),
|
|
861
892
|
default: v.optional(v.string()),
|
|
862
893
|
fallback: v.optional(strings),
|
|
863
894
|
byFile: v.optional(v.record(v.string(), v.string())),
|
|
@@ -889,10 +920,12 @@ const ConfigSchema = v.strictObject({
|
|
|
889
920
|
"nested",
|
|
890
921
|
"flat"
|
|
891
922
|
])),
|
|
923
|
+
keySeparator: v.optional(keySeparator),
|
|
892
924
|
namespace: v.optional(namespaceOptions),
|
|
893
925
|
usage: v.optional(v.strictObject({
|
|
894
926
|
include: v.optional(strings),
|
|
895
927
|
exclude: v.optional(strings),
|
|
928
|
+
gitignore: v.optional(v.boolean()),
|
|
896
929
|
calls: v.optional(v.array(callSpec)),
|
|
897
930
|
regex: v.optional(v.array(regexSpec)),
|
|
898
931
|
ignoreCallees: v.optional(strings),
|
|
@@ -937,7 +970,8 @@ const PluginFileSchema = v.strictObject({
|
|
|
937
970
|
"auto",
|
|
938
971
|
"nested",
|
|
939
972
|
"flat"
|
|
940
|
-
]))
|
|
973
|
+
])),
|
|
974
|
+
keySeparator: v.optional(keySeparator)
|
|
941
975
|
})),
|
|
942
976
|
scanners: v.optional(v.strictObject({
|
|
943
977
|
calls: v.optional(v.array(callSpec)),
|
|
@@ -1137,46 +1171,55 @@ function innermost(scopes, range) {
|
|
|
1137
1171
|
for (const scope of scopes) if (scope.range.start <= range.start && scope.range.end >= range.end && (!best || scope.range.end - scope.range.start < best.range.end - best.range.start)) best = scope;
|
|
1138
1172
|
return best;
|
|
1139
1173
|
}
|
|
1174
|
+
function joinPrefix(prefix, text, joiner) {
|
|
1175
|
+
return text ? `${prefix}${joiner}${text}` : prefix;
|
|
1176
|
+
}
|
|
1177
|
+
/** The first prefix whose key exists. A prefix usage (`items.${id}`) ends in an empty segment, so its parent key decides. */
|
|
1178
|
+
function definedPrefix(prefixes, text, joiner, dynamicPrefix, exists) {
|
|
1179
|
+
return prefixes.find((prefix) => {
|
|
1180
|
+
const prefixed = joinPrefix(prefix, text, joiner);
|
|
1181
|
+
return exists(dynamicPrefix && prefixed.endsWith(joiner) ? prefixed.slice(0, -joiner.length) : prefixed);
|
|
1182
|
+
});
|
|
1183
|
+
}
|
|
1140
1184
|
function createUsageResolver(config, namespaced, normalizers) {
|
|
1141
1185
|
const { separator, fallback } = config.namespace;
|
|
1186
|
+
const joiner = keyJoiner(config.keySeparator);
|
|
1187
|
+
const keyOf = (namespace, text) => namespace === void 0 ? text : `${namespace}:${text}`;
|
|
1188
|
+
const normalize = (raw, ctx) => {
|
|
1189
|
+
for (const normalizer of normalizers) {
|
|
1190
|
+
const result = normalizer(raw, ctx);
|
|
1191
|
+
if (result !== void 0) return result;
|
|
1192
|
+
}
|
|
1193
|
+
return raw;
|
|
1194
|
+
};
|
|
1142
1195
|
const byFile = Object.entries(config.namespace.byFile).map(([glob, namespace]) => ({
|
|
1143
1196
|
matches: pathMatcher([glob]),
|
|
1144
1197
|
namespace
|
|
1145
1198
|
}));
|
|
1146
1199
|
return { resolve(candidate, scopes, path, scanner, has) {
|
|
1147
1200
|
const scope = innermost(scopes, candidate.range);
|
|
1148
|
-
|
|
1149
|
-
let text = candidate.raw;
|
|
1150
|
-
for (const normalize of normalizers) {
|
|
1151
|
-
const result = normalize(text, ctx);
|
|
1152
|
-
if (result !== void 0) {
|
|
1153
|
-
text = result;
|
|
1154
|
-
break;
|
|
1155
|
-
}
|
|
1156
|
-
}
|
|
1201
|
+
let text = normalize(candidate.raw, { path });
|
|
1157
1202
|
const scopeNamespace = candidate.namespace ?? scope?.namespace ?? byFile.find((rule) => rule.matches(path))?.namespace;
|
|
1158
1203
|
let scopePrefix = candidate.keyPrefix ?? scope?.keyPrefix;
|
|
1159
1204
|
let namespace;
|
|
1160
1205
|
let explicit = false;
|
|
1161
1206
|
if (namespaced) {
|
|
1162
|
-
const index = text.indexOf(separator);
|
|
1207
|
+
const index = separator === false ? -1 : text.indexOf(separator);
|
|
1163
1208
|
explicit = index > 0;
|
|
1164
1209
|
if (explicit) {
|
|
1165
1210
|
namespace = text.slice(0, index);
|
|
1166
|
-
text = text.slice(index + separator.length);
|
|
1211
|
+
text = text.slice(index + (separator || "").length);
|
|
1167
1212
|
} else namespace = scopeNamespace ?? config.namespace.default;
|
|
1168
1213
|
}
|
|
1169
1214
|
if (scopePrefix && !explicit) {
|
|
1170
|
-
const
|
|
1171
|
-
|
|
1172
|
-
if (
|
|
1173
|
-
else scopePrefix = void 0;
|
|
1215
|
+
const chosen = !candidate.keyPrefix && scope?.optionalPrefix ? definedPrefix([scopePrefix, ...scope.otherPrefixes ?? []], text, joiner, candidate.dynamic === "prefix", (lookup) => (scope.prefixInFallback ? [namespace, ...fallback] : [namespace]).some((ns) => has(keyOf(ns, lookup)))) : scopePrefix;
|
|
1216
|
+
scopePrefix = chosen;
|
|
1217
|
+
if (chosen) text = joinPrefix(chosen, text, joiner);
|
|
1174
1218
|
}
|
|
1175
|
-
const keyPath = text === "" ? [] : text.split(".");
|
|
1176
1219
|
let key;
|
|
1177
1220
|
if (candidate.dynamic === "unknown") key = void 0;
|
|
1178
|
-
else key =
|
|
1179
|
-
if (key !== void 0 && namespaced && !explicit && candidate.dynamic === false && !has(key)) key = fallback.map((ns) =>
|
|
1221
|
+
else key = keyOf(namespace, text);
|
|
1222
|
+
if (key !== void 0 && namespaced && !explicit && candidate.dynamic === false && !has(key)) key = fallback.map((ns) => keyOf(ns, text)).find(has) ?? key;
|
|
1180
1223
|
return {
|
|
1181
1224
|
key,
|
|
1182
1225
|
raw: candidate.raw,
|
|
@@ -1185,6 +1228,7 @@ function createUsageResolver(config, namespaced, normalizers) {
|
|
|
1185
1228
|
expression: candidate.expression,
|
|
1186
1229
|
...candidate.params ? { params: candidate.params } : {},
|
|
1187
1230
|
...candidate.defaultValue === void 0 ? {} : { defaultValue: candidate.defaultValue },
|
|
1231
|
+
...candidate.description === void 0 ? {} : { description: candidate.description },
|
|
1188
1232
|
scanner,
|
|
1189
1233
|
kind: candidate.kind,
|
|
1190
1234
|
dynamic: candidate.dynamic,
|
|
@@ -1328,12 +1372,19 @@ function computeCoverage(state, options, sourceKeys) {
|
|
|
1328
1372
|
const missing = [];
|
|
1329
1373
|
const empty = [];
|
|
1330
1374
|
const outdated = [];
|
|
1375
|
+
const needsReview = [];
|
|
1376
|
+
const reviewed = [];
|
|
1331
1377
|
for (const key of reference) {
|
|
1332
|
-
const
|
|
1378
|
+
const message = messages.get(key)?.get(locale)?.[0];
|
|
1379
|
+
const value = message?.value;
|
|
1333
1380
|
if (value === void 0 || value === "") {
|
|
1334
1381
|
if (inherited(key)) continue;
|
|
1335
1382
|
(value === void 0 ? missing : empty).push(key);
|
|
1336
|
-
|
|
1383
|
+
continue;
|
|
1384
|
+
}
|
|
1385
|
+
if (locale !== sourceLocale && isOutdated(state.history(key, sourceLocale), state.history(key, locale)) && !state.fresh(key, locale)) outdated.push(key);
|
|
1386
|
+
if (message?.state === "needs-review") needsReview.push(key);
|
|
1387
|
+
else if (message?.state === "reviewed") reviewed.push(key);
|
|
1337
1388
|
}
|
|
1338
1389
|
return {
|
|
1339
1390
|
locale,
|
|
@@ -1341,7 +1392,9 @@ function computeCoverage(state, options, sourceKeys) {
|
|
|
1341
1392
|
translated: reference.length - missing.length - empty.length,
|
|
1342
1393
|
missing,
|
|
1343
1394
|
empty,
|
|
1344
|
-
outdated
|
|
1395
|
+
outdated,
|
|
1396
|
+
needsReview,
|
|
1397
|
+
reviewed
|
|
1345
1398
|
};
|
|
1346
1399
|
});
|
|
1347
1400
|
}
|
|
@@ -1425,8 +1478,8 @@ function runChecks(input) {
|
|
|
1425
1478
|
function createAnalyzer(options) {
|
|
1426
1479
|
const { config, registry } = options;
|
|
1427
1480
|
let memo;
|
|
1428
|
-
const ignored = keyMatcher(config.ignoreKeys);
|
|
1429
|
-
const inUse = keyMatcher(config.usage.keysInUse);
|
|
1481
|
+
const ignored = keyMatcher(config.ignoreKeys, config.keySeparator);
|
|
1482
|
+
const inUse = keyMatcher(config.usage.keysInUse, config.keySeparator);
|
|
1430
1483
|
const placeholderCache = /* @__PURE__ */ new Map();
|
|
1431
1484
|
function analyze() {
|
|
1432
1485
|
const state = options.state();
|
|
@@ -1684,19 +1737,19 @@ function nodeAt(tree, path) {
|
|
|
1684
1737
|
return node;
|
|
1685
1738
|
}
|
|
1686
1739
|
/** True for flat files (`{ "a.b": "x" }`), false for nested ones, undefined when it cannot tell. */
|
|
1687
|
-
function isFlatTree(tree) {
|
|
1740
|
+
function isFlatTree(tree, separator) {
|
|
1688
1741
|
const entries = Object.entries(tree);
|
|
1689
1742
|
if (entries.length === 0) return void 0;
|
|
1690
1743
|
if (entries.some(([, value]) => !isMessageLeaf(value))) return false;
|
|
1691
|
-
return entries.some(([key]) => key.includes(
|
|
1744
|
+
return entries.some(([key]) => key.includes(separator)) ? true : void 0;
|
|
1692
1745
|
}
|
|
1693
1746
|
/** The longest key prefix a tree shares with `path`. */
|
|
1694
|
-
function longestPrefix(tree, prefix, path) {
|
|
1747
|
+
function longestPrefix(tree, prefix, path, separator) {
|
|
1695
1748
|
let best = 0;
|
|
1696
1749
|
for (const [segment, child] of Object.entries(tree)) {
|
|
1697
|
-
const next = [...prefix, ...segment
|
|
1750
|
+
const next = [...prefix, ...splitKeyPath(segment, separator)];
|
|
1698
1751
|
best = Math.max(best, commonPrefix(next, path));
|
|
1699
|
-
if (!isMessageLeaf(child)) best = Math.max(best, longestPrefix(child, next, path));
|
|
1752
|
+
if (!isMessageLeaf(child)) best = Math.max(best, longestPrefix(child, next, path, separator));
|
|
1700
1753
|
}
|
|
1701
1754
|
return best;
|
|
1702
1755
|
}
|
|
@@ -1729,6 +1782,10 @@ var PlanBuilder = class {
|
|
|
1729
1782
|
answers;
|
|
1730
1783
|
work = /* @__PURE__ */ new Map();
|
|
1731
1784
|
created = /* @__PURE__ */ new Map();
|
|
1785
|
+
/** New files that a finder's `resolveKey` gave one key. */
|
|
1786
|
+
keyFiles = /* @__PURE__ */ new Map();
|
|
1787
|
+
/** Resources that are the file of a key deleted from them: removed once empty. */
|
|
1788
|
+
keyFilesLeft = /* @__PURE__ */ new Set();
|
|
1732
1789
|
sourceEdits = /* @__PURE__ */ new Map();
|
|
1733
1790
|
sourceImports = /* @__PURE__ */ new Map();
|
|
1734
1791
|
decisions = [];
|
|
@@ -1755,6 +1812,7 @@ var PlanBuilder = class {
|
|
|
1755
1812
|
path: s.resource.path,
|
|
1756
1813
|
locale: s.resource.locale,
|
|
1757
1814
|
namespace: s.resource.namespace,
|
|
1815
|
+
keySeparator: s.resource.keySeparator,
|
|
1758
1816
|
format,
|
|
1759
1817
|
state: s
|
|
1760
1818
|
};
|
|
@@ -1779,24 +1837,36 @@ var PlanBuilder = class {
|
|
|
1779
1837
|
return s;
|
|
1780
1838
|
}
|
|
1781
1839
|
keyId(key) {
|
|
1782
|
-
const id = splitKey(key, this.state.namespaced);
|
|
1840
|
+
const id = splitKey(key, this.state.namespaced, this.config.keySeparator);
|
|
1783
1841
|
return {
|
|
1784
1842
|
namespace: id.namespace,
|
|
1785
1843
|
path: [...id.path]
|
|
1786
1844
|
};
|
|
1787
1845
|
}
|
|
1788
|
-
|
|
1846
|
+
separatorOf(target) {
|
|
1847
|
+
return keySeparatorOf(target, this.config.keySeparator);
|
|
1848
|
+
}
|
|
1849
|
+
/**
|
|
1850
|
+
* Key path relative to a resource, or undefined when the key does not belong there.
|
|
1851
|
+
* A resource of literal keys (`keySeparator: false`) gets the whole rest of the key as one segment.
|
|
1852
|
+
*/
|
|
1789
1853
|
relativePath(key, target) {
|
|
1790
|
-
const { namespace,
|
|
1854
|
+
const { namespace, rest } = splitNamespace(key, this.state.namespaced);
|
|
1855
|
+
const literal = this.separatorOf(target) === false;
|
|
1856
|
+
const joiner = keyJoiner(this.config.keySeparator);
|
|
1857
|
+
const split = (text) => splitKeyPath(text, literal ? false : joiner);
|
|
1791
1858
|
const fallback = this.config.namespace.default;
|
|
1792
|
-
if (this.state.namespaced) return (namespace ?? fallback) === (target.namespace ?? fallback) ?
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1859
|
+
if (this.state.namespaced) return (namespace ?? fallback) === (target.namespace ?? fallback) ? split(rest) : void 0;
|
|
1860
|
+
if (!target.namespace) return rest === "" ? void 0 : split(rest);
|
|
1861
|
+
const prefix = target.namespace.split(".").join(joiner) + joiner;
|
|
1862
|
+
if (rest.length <= prefix.length || !rest.startsWith(prefix)) return void 0;
|
|
1863
|
+
return split(rest.slice(prefix.length));
|
|
1796
1864
|
}
|
|
1797
1865
|
storedPathFor(entry, relative) {
|
|
1798
1866
|
const stored = this.options.toStored(entry.target, relative);
|
|
1799
|
-
|
|
1867
|
+
const separator = this.separatorOf(entry.target);
|
|
1868
|
+
if (separator === false) return stored;
|
|
1869
|
+
return stored.length === relative.length && (isFlatTree(entry.tree, separator) ?? this.config.keyStyle === "flat") ? [stored.join(separator)] : stored;
|
|
1800
1870
|
}
|
|
1801
1871
|
createTarget(key, locale) {
|
|
1802
1872
|
const { resources, sourceLocale, namespaced } = this.state;
|
|
@@ -1816,6 +1886,7 @@ var PlanBuilder = class {
|
|
|
1816
1886
|
path: resolved,
|
|
1817
1887
|
locale,
|
|
1818
1888
|
namespace,
|
|
1889
|
+
keySeparator: model?.resource.keySeparator,
|
|
1819
1890
|
format,
|
|
1820
1891
|
state: void 0
|
|
1821
1892
|
};
|
|
@@ -1835,14 +1906,47 @@ var PlanBuilder = class {
|
|
|
1835
1906
|
path,
|
|
1836
1907
|
locale,
|
|
1837
1908
|
namespace: namespace ?? model.resource.namespace,
|
|
1909
|
+
keySeparator: model.resource.keySeparator,
|
|
1838
1910
|
format,
|
|
1839
1911
|
state: void 0
|
|
1840
1912
|
};
|
|
1841
1913
|
this.created.set(path, target);
|
|
1842
1914
|
return target;
|
|
1843
1915
|
}
|
|
1844
|
-
/**
|
|
1916
|
+
/** The file a locale finder gives the key, created when it does not exist. */
|
|
1917
|
+
keyTarget(key, locale) {
|
|
1918
|
+
const file = this.options.resolveKey(key, locale);
|
|
1919
|
+
if (!file) return void 0;
|
|
1920
|
+
const existing = this.state.resources.find((s) => s.resource.path === file.path && !s.resource.block);
|
|
1921
|
+
if (existing) {
|
|
1922
|
+
if (existing.resource.locale !== locale || this.relativePath(key, existing.resource) === void 0) throw errors.X18N4005({
|
|
1923
|
+
key,
|
|
1924
|
+
locale
|
|
1925
|
+
});
|
|
1926
|
+
return this.targetOf(existing);
|
|
1927
|
+
}
|
|
1928
|
+
const pending = this.keyFiles.get(file.path);
|
|
1929
|
+
if (pending) return pending;
|
|
1930
|
+
const format = file.format === void 0 ? this.options.registry.formatFor(file.path) : this.options.registry.formatById(file.format);
|
|
1931
|
+
if (!format?.serialize) throw errors.X18N4005({
|
|
1932
|
+
key,
|
|
1933
|
+
locale
|
|
1934
|
+
});
|
|
1935
|
+
const target = {
|
|
1936
|
+
id: file.path,
|
|
1937
|
+
path: file.path,
|
|
1938
|
+
locale,
|
|
1939
|
+
namespace: this.state.namespaced ? this.keyId(key).namespace ?? this.config.namespace.default : void 0,
|
|
1940
|
+
format,
|
|
1941
|
+
state: void 0
|
|
1942
|
+
};
|
|
1943
|
+
this.keyFiles.set(file.path, target);
|
|
1944
|
+
return target;
|
|
1945
|
+
}
|
|
1946
|
+
/** Placement: the key's own file, then the namespace's resource, then the longest shared prefix, then the first; a tie is a decision. */
|
|
1845
1947
|
pickTarget(key, locale) {
|
|
1948
|
+
const own = this.keyTarget(key, locale);
|
|
1949
|
+
if (own) return own;
|
|
1846
1950
|
let inLocale = this.state.resources.filter((s) => s.resource.locale === locale && this.relativePath(key, s.resource) !== void 0);
|
|
1847
1951
|
const source = this.options.message(key, this.state.sourceLocale);
|
|
1848
1952
|
const sourceNamespace = source && this.stateById.get(source.resource)?.resource.namespace;
|
|
@@ -1854,7 +1958,7 @@ var PlanBuilder = class {
|
|
|
1854
1958
|
const prefix = s.resource.namespace && !this.state.namespaced ? path.slice(0, s.resource.namespace.split(".").length) : [];
|
|
1855
1959
|
return {
|
|
1856
1960
|
s,
|
|
1857
|
-
best: longestPrefix(s.tree, prefix, path)
|
|
1961
|
+
best: longestPrefix(s.tree, prefix, path, this.separatorOf(s.resource))
|
|
1858
1962
|
};
|
|
1859
1963
|
});
|
|
1860
1964
|
const top = Math.max(...scores.map((x) => x.best));
|
|
@@ -1901,7 +2005,9 @@ var PlanBuilder = class {
|
|
|
1901
2005
|
});
|
|
1902
2006
|
}
|
|
1903
2007
|
deleteMessage(m) {
|
|
1904
|
-
const
|
|
2008
|
+
const s = this.writable(m.resource);
|
|
2009
|
+
if (!s.resource.block && this.options.resolveKey(m.key, m.locale)?.path === s.resource.path) this.keyFilesLeft.add(s.resource.id);
|
|
2010
|
+
const entry = this.workFor(this.targetOf(s));
|
|
1905
2011
|
let path = [...m.storedPath];
|
|
1906
2012
|
while (path.length > 1) {
|
|
1907
2013
|
const parent = nodeAt(entry.tree, path.slice(0, -1));
|
|
@@ -1960,14 +2066,16 @@ var PlanBuilder = class {
|
|
|
1960
2066
|
if (renamed.has(m.resource)) continue;
|
|
1961
2067
|
const s = this.writable(m.resource);
|
|
1962
2068
|
const relative = this.relativePath(to, s.resource);
|
|
1963
|
-
|
|
2069
|
+
const own = this.options.resolveKey(to, locale);
|
|
2070
|
+
if (!relative || own && own.path !== s.resource.path) {
|
|
1964
2071
|
this.deleteMessage(m);
|
|
1965
2072
|
this.setValue(to, locale, m.value);
|
|
1966
2073
|
continue;
|
|
1967
2074
|
}
|
|
1968
2075
|
const entry = this.workFor(this.targetOf(s));
|
|
1969
|
-
const
|
|
1970
|
-
const
|
|
2076
|
+
const separator = this.separatorOf(s.resource);
|
|
2077
|
+
const flat = separator !== false && m.storedPath.length === 1 && m.storedPath[0].includes(separator);
|
|
2078
|
+
const next = flat ? [relative.join(separator)] : this.options.toStored(s.resource, relative);
|
|
1971
2079
|
const sameParent = next.length === m.storedPath.length && next.slice(0, -1).every((segment, i) => segment === m.storedPath[i]);
|
|
1972
2080
|
const last = next.at(-1);
|
|
1973
2081
|
if (sameParent && m.range && (flat ? safeFlat : safeSegment).test(last)) {
|
|
@@ -1999,16 +2107,17 @@ var PlanBuilder = class {
|
|
|
1999
2107
|
const scope = usage.key.slice(0, usage.key.length - usage.raw.length);
|
|
2000
2108
|
if (next.startsWith(`${scope}.`)) return next.slice(scope.length);
|
|
2001
2109
|
}
|
|
2002
|
-
const { namespace,
|
|
2003
|
-
let rest =
|
|
2110
|
+
const { namespace, rest: text } = splitNamespace(next, this.state.namespaced);
|
|
2111
|
+
let rest = text;
|
|
2004
2112
|
const separator = this.config.namespace.separator;
|
|
2005
|
-
const explicit = this.state.namespaced && usage.raw.indexOf(separator) > 0;
|
|
2113
|
+
const explicit = this.state.namespaced && separator !== false && usage.raw.indexOf(separator) > 0;
|
|
2006
2114
|
if (usage.scopePrefix && !explicit) {
|
|
2007
|
-
|
|
2008
|
-
|
|
2115
|
+
const prefix = usage.scopePrefix + keyJoiner(this.config.keySeparator);
|
|
2116
|
+
if (!rest.startsWith(prefix)) return void 0;
|
|
2117
|
+
rest = rest.slice(prefix.length);
|
|
2009
2118
|
}
|
|
2010
2119
|
if (!this.state.namespaced) return rest;
|
|
2011
|
-
if (explicit || namespace !== (usage.scopeNamespace ?? this.config.namespace.default)) return `${namespace}${separator}${rest}`;
|
|
2120
|
+
if (explicit || namespace !== (usage.scopeNamespace ?? this.config.namespace.default)) return separator === false ? void 0 : `${namespace}${separator}${rest}`;
|
|
2012
2121
|
return rest;
|
|
2013
2122
|
}
|
|
2014
2123
|
editSource(path, edit) {
|
|
@@ -2053,7 +2162,7 @@ var PlanBuilder = class {
|
|
|
2053
2162
|
if (op.from === op.to) return;
|
|
2054
2163
|
const all = this.options.allKeys();
|
|
2055
2164
|
const isLeaf = this.options.messages(op.from) !== void 0;
|
|
2056
|
-
const under = all.filter((key) => isKeyUnder(key, op.from));
|
|
2165
|
+
const under = all.filter((key) => isKeyUnder(key, op.from, this.config.keySeparator));
|
|
2057
2166
|
const derived = all.filter((key) => this.options.basesOf(key).includes(op.from));
|
|
2058
2167
|
let keys;
|
|
2059
2168
|
if (op.type === "rename-prefix") keys = under;
|
|
@@ -2073,7 +2182,7 @@ var PlanBuilder = class {
|
|
|
2073
2182
|
this.rewritePrefixUsages(op.from, op.to);
|
|
2074
2183
|
}
|
|
2075
2184
|
delete(op) {
|
|
2076
|
-
const keys = this.options.messages(op.key) ? [op.key] : this.options.allKeys().filter((key) => isKeyUnder(key, op.key));
|
|
2185
|
+
const keys = this.options.messages(op.key) ? [op.key] : this.options.allKeys().filter((key) => isKeyUnder(key, op.key, this.config.keySeparator));
|
|
2077
2186
|
if (keys.length === 0) throw errors.X18N4006({ key: op.key });
|
|
2078
2187
|
for (const key of keys) {
|
|
2079
2188
|
this.touched.add(key);
|
|
@@ -2267,6 +2376,14 @@ var PlanBuilder = class {
|
|
|
2267
2376
|
texts.set(target.path, void 0);
|
|
2268
2377
|
continue;
|
|
2269
2378
|
}
|
|
2379
|
+
if (this.keyFilesLeft.has(s.resource.id) && Object.keys(entry.tree).length === 0) {
|
|
2380
|
+
changes.push({
|
|
2381
|
+
type: "delete",
|
|
2382
|
+
path: target.path
|
|
2383
|
+
});
|
|
2384
|
+
texts.set(target.path, this.state.fileTexts.get(target.path));
|
|
2385
|
+
continue;
|
|
2386
|
+
}
|
|
2270
2387
|
const edits = diffText(s.content, this.nextContent(entry, s)).map((e) => ({
|
|
2271
2388
|
range: {
|
|
2272
2389
|
start: e.range.start + s.offset,
|
|
@@ -2403,7 +2520,7 @@ function createPlanner(options) {
|
|
|
2403
2520
|
for (const change of fresh.plan.changes) {
|
|
2404
2521
|
const current = await options.host.readFile(change.path);
|
|
2405
2522
|
const expected = fresh.texts.get(change.path);
|
|
2406
|
-
if (change.type
|
|
2523
|
+
if (change.type !== "create" && expected !== void 0 && current !== expected || change.type === "create" && current !== void 0) throw errors.X18N4004({ path: change.path });
|
|
2407
2524
|
}
|
|
2408
2525
|
const events = [];
|
|
2409
2526
|
for (const change of fresh.plan.changes) events.push(await write(change));
|
|
@@ -2417,6 +2534,10 @@ function createPlanner(options) {
|
|
|
2417
2534
|
}
|
|
2418
2535
|
//#endregion
|
|
2419
2536
|
//#region src/project/prompt.ts
|
|
2537
|
+
/** Related locales shown per message, at most. */
|
|
2538
|
+
const RELATED_LIMIT = 2;
|
|
2539
|
+
/** Characters of a description or a related value, at most; longer text is cut. */
|
|
2540
|
+
const TEXT_LIMIT = 300;
|
|
2420
2541
|
function text(value) {
|
|
2421
2542
|
return value === void 0 || value === null ? void 0 : String(value);
|
|
2422
2543
|
}
|
|
@@ -2451,10 +2572,36 @@ function reportedValues(project, diagnostics, to, check) {
|
|
|
2451
2572
|
}
|
|
2452
2573
|
return reported;
|
|
2453
2574
|
}
|
|
2575
|
+
function clip(value) {
|
|
2576
|
+
return value.length > TEXT_LIMIT ? `${value.slice(0, 299)}…` : value;
|
|
2577
|
+
}
|
|
2578
|
+
const subtagsOf = (code) => code.toLowerCase().split(/[-_]/);
|
|
2579
|
+
/**
|
|
2580
|
+
* The locales whose values help translate to `to`, nearest first: the fallback chain of `to`, then the other locales of
|
|
2581
|
+
* the same language, those that share more subtags with `to` first, then the shorter codes (`pt` and `pt-PT` for `pt-BR`,
|
|
2582
|
+
* `pt-BR` for `pt`). The source locale and `to` itself are never related.
|
|
2583
|
+
*/
|
|
2584
|
+
function relatedLocales(to, locales, sourceLocale, fallbacks) {
|
|
2585
|
+
const target = subtagsOf(to);
|
|
2586
|
+
const shared = (code) => {
|
|
2587
|
+
const tags = subtagsOf(code);
|
|
2588
|
+
let n = 0;
|
|
2589
|
+
while (n < tags.length && tags[n] === target[n]) n++;
|
|
2590
|
+
return n;
|
|
2591
|
+
};
|
|
2592
|
+
const chain = fallbackChain(fallbacks, to).filter((code) => code !== sourceLocale && code !== to);
|
|
2593
|
+
const language = locales.filter((code) => code !== to && code !== sourceLocale && !chain.includes(code) && shared(code) > 0).sort((a, b) => shared(b) - shared(a) || subtagsOf(a).length - subtagsOf(b).length || a.localeCompare(b));
|
|
2594
|
+
return [...chain, ...language];
|
|
2595
|
+
}
|
|
2596
|
+
/** The description of a key: from the source resource, else another locale's, else a usage's. */
|
|
2597
|
+
function descriptionOf(project, key, from) {
|
|
2598
|
+
const trimmed = (project.message(key, from)?.description ?? project.locales.map((locale) => project.message(key, locale)?.description).find(Boolean) ?? project.usages({ key }).map(({ usage }) => usage.description).find(Boolean))?.replace(/\s+/g, " ").trim();
|
|
2599
|
+
return trimmed ? clip(trimmed) : void 0;
|
|
2600
|
+
}
|
|
2454
2601
|
/** Builds the parts of a translate or review prompt from project state. */
|
|
2455
2602
|
function buildPrompt(project, kind, to, options, extra) {
|
|
2456
2603
|
const from = project.sourceLocale;
|
|
2457
|
-
const match = typeof options.keys === "string" ? keyMatcher([options.keys]) : void 0;
|
|
2604
|
+
const match = typeof options.keys === "string" ? keyMatcher([options.keys], extra.config.keySeparator) : void 0;
|
|
2458
2605
|
const list = Array.isArray(options.keys) ? new Set(options.keys) : void 0;
|
|
2459
2606
|
const analysis = project.analyze();
|
|
2460
2607
|
const outdated = new Set(analysis.coverage.find((c) => c.locale === to)?.outdated);
|
|
@@ -2475,12 +2622,24 @@ function buildPrompt(project, kind, to, options, extra) {
|
|
|
2475
2622
|
const end = start + page.length;
|
|
2476
2623
|
const syntaxes = project.placeholderSyntaxes;
|
|
2477
2624
|
const info = project.localeInfo(to);
|
|
2625
|
+
const related = options.related === false ? [] : Array.isArray(options.related) ? options.related.filter((code) => code !== to && code !== from) : relatedLocales(to, project.locales, from, extra.fallbacks);
|
|
2626
|
+
const relatedOf = (key, source) => related.flatMap((locale) => {
|
|
2627
|
+
const value = text(project.message(key, locale)?.value);
|
|
2628
|
+
return value && value !== source ? [{
|
|
2629
|
+
locale,
|
|
2630
|
+
value: clip(value)
|
|
2631
|
+
}] : [];
|
|
2632
|
+
}).slice(0, RELATED_LIMIT);
|
|
2633
|
+
const relatedValues = new Map(page.map((key) => [key, relatedOf(key, text(project.message(key, from)?.value) ?? "")]));
|
|
2634
|
+
const descriptions = new Map(page.map((key) => [key, descriptionOf(project, key, from)]));
|
|
2478
2635
|
const draft = {
|
|
2479
2636
|
rules: [],
|
|
2480
2637
|
notes: []
|
|
2481
2638
|
};
|
|
2482
2639
|
if (syntaxes.length > 0) draft.rules.push(`Keep every placeholder exactly as written (${syntaxes.map((s) => `\`${s.example ?? s.id}\``).join(", ")}); do not translate their names.`);
|
|
2483
2640
|
draft.rules.push("Keep markup tags and their attributes as they are.", "Match the tone and wording of the existing messages in the target locale.", "Do not translate glossary terms marked \"do not translate\".");
|
|
2641
|
+
if ([...descriptions.values()].some(Boolean)) draft.rules.push("A description tells what a message means or where it appears: follow it, and do not translate it.");
|
|
2642
|
+
if ([...relatedValues.values()].some((values) => values.length > 0)) draft.rules.push(`Values in related locales are references: keep what fits ${to}, and change the spelling, words and forms that differ.`);
|
|
2484
2643
|
if (reported.size > 0) draft.rules.push(`A message with a reported problem has a current ${to} value: fix the problem. When the current value is right as it is, such as a word that is the same in both languages, leave the key out of the results.`);
|
|
2485
2644
|
if (info.pluralCategories.length > 1) draft.notes.push(`${to} uses the plural categories ${info.pluralCategories.join(", ")}.`);
|
|
2486
2645
|
for (const hook of extra.hooks) hook(draft, {
|
|
@@ -2501,10 +2660,13 @@ function buildPrompt(project, kind, to, options, extra) {
|
|
|
2501
2660
|
const source = text(project.message(key, from)?.value) ?? "";
|
|
2502
2661
|
const translated = project.history(key, to);
|
|
2503
2662
|
const previous = translated && stale(key) ? text(project.valueAt(key, from, translated.updatedAt)) : void 0;
|
|
2663
|
+
const values = relatedValues.get(key) ?? [];
|
|
2504
2664
|
return {
|
|
2505
2665
|
key,
|
|
2506
2666
|
source,
|
|
2507
2667
|
placeholders: [...new Set(syntaxes.flatMap((s) => s.find(source)).map((p) => p.text))],
|
|
2668
|
+
description: descriptions.get(key),
|
|
2669
|
+
related: values.length > 0 ? values : void 0,
|
|
2508
2670
|
existing: text(project.message(key, to)?.value) || void 0,
|
|
2509
2671
|
previousSource: previous === source ? void 0 : previous,
|
|
2510
2672
|
issues: reported.get(key)?.map((d) => ({
|
|
@@ -2553,6 +2715,8 @@ function renderItem(item, to) {
|
|
|
2553
2715
|
`- Source: ${JSON.stringify(item.source)}`
|
|
2554
2716
|
];
|
|
2555
2717
|
if (item.placeholders.length) out.push(`- Placeholders: ${item.placeholders.map((p) => `\`${p}\``).join(", ")}`);
|
|
2718
|
+
if (item.description !== void 0) out.push(`- Description: ${JSON.stringify(item.description)}`);
|
|
2719
|
+
for (const value of item.related ?? []) out.push(`- In ${value.locale}: ${JSON.stringify(value.value)}`);
|
|
2556
2720
|
if (item.previousSource !== void 0) out.push(`- Source when ${to} last changed: ${JSON.stringify(item.previousSource)}`);
|
|
2557
2721
|
if (item.existing !== void 0) out.push(`- Current ${to}: ${JSON.stringify(item.existing)}`);
|
|
2558
2722
|
for (const issue of item.issues ?? []) out.push(`- Reported: ${issue.message} (\`${issue.code}\`)`);
|
|
@@ -2584,6 +2748,80 @@ function renderPrompt(data) {
|
|
|
2584
2748
|
return out.join("\n");
|
|
2585
2749
|
}
|
|
2586
2750
|
//#endregion
|
|
2751
|
+
//#region src/project/unused.ts
|
|
2752
|
+
/**
|
|
2753
|
+
* True when a fully dynamic usage (`t(key)`) may reach `key`: the key lies in the usage's scope namespace and under its
|
|
2754
|
+
* scope key prefix. Undefined for a usage without a scope, which may reach any key.
|
|
2755
|
+
*/
|
|
2756
|
+
function scopeMatcher(usage, namespaced) {
|
|
2757
|
+
const namespace = namespaced ? usage.scopeNamespace : void 0;
|
|
2758
|
+
const prefix = usage.scopePrefix;
|
|
2759
|
+
if (namespace === void 0 && !prefix) return void 0;
|
|
2760
|
+
return (key) => {
|
|
2761
|
+
const id = splitKey(key, namespaced);
|
|
2762
|
+
if (namespace !== void 0 && id.namespace !== namespace) return false;
|
|
2763
|
+
if (!prefix) return true;
|
|
2764
|
+
const path = id.path.join(".");
|
|
2765
|
+
return path === prefix || path.startsWith(`${prefix}.`);
|
|
2766
|
+
};
|
|
2767
|
+
}
|
|
2768
|
+
function planUnusedDeletion(options, query = {}) {
|
|
2769
|
+
const ignored = keyMatcher(options.ignoreKeys);
|
|
2770
|
+
const match = query.glob ? keyMatcher([query.glob]) : void 0;
|
|
2771
|
+
const candidates = [...options.unused].filter((key) => !ignored(key) && (match === void 0 || match(key)) && (query.namespace === void 0 || splitKey(key, options.namespaced).namespace === query.namespace)).sort();
|
|
2772
|
+
const scoped = [];
|
|
2773
|
+
const unscoped = [];
|
|
2774
|
+
for (const usage of options.usages) {
|
|
2775
|
+
if (usage.dynamic !== "unknown") continue;
|
|
2776
|
+
const reaches = scopeMatcher(usage, options.namespaced);
|
|
2777
|
+
if (reaches) scoped.push({
|
|
2778
|
+
usage,
|
|
2779
|
+
reaches
|
|
2780
|
+
});
|
|
2781
|
+
else unscoped.push(usage);
|
|
2782
|
+
}
|
|
2783
|
+
const keys = [];
|
|
2784
|
+
const kept = [];
|
|
2785
|
+
for (const key of candidates) {
|
|
2786
|
+
const usages = scoped.filter((s) => s.reaches(key)).map((s) => s.usage);
|
|
2787
|
+
if (usages.length > 0) {
|
|
2788
|
+
kept.push({
|
|
2789
|
+
key,
|
|
2790
|
+
usages
|
|
2791
|
+
});
|
|
2792
|
+
continue;
|
|
2793
|
+
}
|
|
2794
|
+
keys.push({
|
|
2795
|
+
key,
|
|
2796
|
+
messages: options.locations(key).filter((node) => node.leaf).map((node) => ({
|
|
2797
|
+
locale: node.locale,
|
|
2798
|
+
path: node.path
|
|
2799
|
+
}))
|
|
2800
|
+
});
|
|
2801
|
+
}
|
|
2802
|
+
const plan = options.plan(keys.map(({ key }) => ({
|
|
2803
|
+
type: "delete",
|
|
2804
|
+
key,
|
|
2805
|
+
locales: "all"
|
|
2806
|
+
})));
|
|
2807
|
+
if (unscoped.length === 0 || keys.length === 0) return {
|
|
2808
|
+
plan,
|
|
2809
|
+
keys,
|
|
2810
|
+
kept
|
|
2811
|
+
};
|
|
2812
|
+
const files = [...new Set(unscoped.map((usage) => usage.path))].sort();
|
|
2813
|
+
const shown = files.length > 5 ? `${files.slice(0, 5).join(", ")} and ${files.length - 5} more files` : files.join(", ");
|
|
2814
|
+
const warning = `${unscoped.length} usage${unscoped.length === 1 ? " has" : "s have"} a fully dynamic key (in ${shown}) that may reach any key; list the keys they use in \`usage.keysInUse\` to keep them.`;
|
|
2815
|
+
return {
|
|
2816
|
+
plan: {
|
|
2817
|
+
...plan,
|
|
2818
|
+
warnings: [...plan.warnings, warning]
|
|
2819
|
+
},
|
|
2820
|
+
keys,
|
|
2821
|
+
kept
|
|
2822
|
+
};
|
|
2823
|
+
}
|
|
2824
|
+
//#endregion
|
|
2587
2825
|
//#region src/project/project.ts
|
|
2588
2826
|
const ioConcurrency = 64;
|
|
2589
2827
|
async function forEachLimit(items, limit, fn) {
|
|
@@ -2691,12 +2929,15 @@ function createProject(options) {
|
|
|
2691
2929
|
const source = message(key, sourceLocale)?.value;
|
|
2692
2930
|
return recorded !== void 0 && source !== void 0 && recorded === sourceHash(source);
|
|
2693
2931
|
}
|
|
2694
|
-
|
|
2695
|
-
|
|
2932
|
+
function finderContext() {
|
|
2933
|
+
return {
|
|
2696
2934
|
host,
|
|
2697
2935
|
config,
|
|
2698
2936
|
extensions: [...new Set(registry.formats.flatMap((format) => format.extensions))]
|
|
2699
2937
|
};
|
|
2938
|
+
}
|
|
2939
|
+
async function discover() {
|
|
2940
|
+
const ctx = finderContext();
|
|
2700
2941
|
const seen = /* @__PURE__ */ new Map();
|
|
2701
2942
|
const out = [];
|
|
2702
2943
|
duplicates = [];
|
|
@@ -2725,7 +2966,8 @@ function createProject(options) {
|
|
|
2725
2966
|
format: candidate.format,
|
|
2726
2967
|
block: candidate.block,
|
|
2727
2968
|
readonly: candidate.readonly === true,
|
|
2728
|
-
finder: finder.id
|
|
2969
|
+
finder: finder.id,
|
|
2970
|
+
keySeparator: candidate.keySeparator === config.keySeparator ? void 0 : candidate.keySeparator
|
|
2729
2971
|
});
|
|
2730
2972
|
}
|
|
2731
2973
|
finderResults.push({
|
|
@@ -2735,10 +2977,22 @@ function createProject(options) {
|
|
|
2735
2977
|
}
|
|
2736
2978
|
return out;
|
|
2737
2979
|
}
|
|
2980
|
+
/** Namespace and key path of a stored path: each stored segment is split by the resource's key separator. */
|
|
2981
|
+
function keyIdFor(resource, stored) {
|
|
2982
|
+
const separator = keySeparatorOf(resource, config.keySeparator);
|
|
2983
|
+
const logical = stored.flatMap((segment) => splitKeyPath(segment, separator));
|
|
2984
|
+
if (namespaced) return {
|
|
2985
|
+
namespace: resource.namespace ?? config.namespace.default,
|
|
2986
|
+
path: logical
|
|
2987
|
+
};
|
|
2988
|
+
return {
|
|
2989
|
+
namespace: void 0,
|
|
2990
|
+
path: [...resource.namespace ? resource.namespace.split(".") : [], ...logical]
|
|
2991
|
+
};
|
|
2992
|
+
}
|
|
2738
2993
|
function keyFor(resource, stored) {
|
|
2739
|
-
const
|
|
2740
|
-
|
|
2741
|
-
return joinKey(void 0, [...resource.namespace ? resource.namespace.split(".") : [], ...logical]);
|
|
2994
|
+
const { namespace, path } = keyIdFor(resource, stored);
|
|
2995
|
+
return joinKey(namespace, path, config.keySeparator);
|
|
2742
2996
|
}
|
|
2743
2997
|
/** Key path to stored path through the transforms that apply to a resource, last first. */
|
|
2744
2998
|
function storedPathOf(ctx) {
|
|
@@ -2756,7 +3010,8 @@ function createProject(options) {
|
|
|
2756
3010
|
format: format?.id ?? "unknown",
|
|
2757
3011
|
block: candidate.block,
|
|
2758
3012
|
readonly: candidate.readonly || !format || !format.edit && !format.serialize,
|
|
2759
|
-
finder: candidate.finder
|
|
3013
|
+
finder: candidate.finder,
|
|
3014
|
+
...candidate.keySeparator === void 0 ? {} : { keySeparator: candidate.keySeparator }
|
|
2760
3015
|
};
|
|
2761
3016
|
const content = candidate.block ? text.slice(candidate.block.range.start, candidate.block.range.end) : text;
|
|
2762
3017
|
const offset = candidate.block?.range.start ?? 0;
|
|
@@ -2778,18 +3033,29 @@ function createProject(options) {
|
|
|
2778
3033
|
tree: {},
|
|
2779
3034
|
rawTree: {},
|
|
2780
3035
|
ranges: /* @__PURE__ */ new Map(),
|
|
3036
|
+
descriptions: /* @__PURE__ */ new Map(),
|
|
3037
|
+
states: /* @__PURE__ */ new Map(),
|
|
3038
|
+
labels: /* @__PURE__ */ new Map(),
|
|
2781
3039
|
error: errors.X18N2001({ path: candidate.path })
|
|
2782
3040
|
};
|
|
2783
3041
|
try {
|
|
2784
3042
|
const parsed = format.parse(content, ctx);
|
|
2785
3043
|
let tree = parsed.tree;
|
|
2786
|
-
|
|
3044
|
+
const descriptions = new Map(parsed.descriptions);
|
|
3045
|
+
for (const transform of transforms) {
|
|
3046
|
+
if (transform.applies?.(ctx) === false) continue;
|
|
3047
|
+
tree = transform.read(tree, ctx);
|
|
3048
|
+
for (const [id, description] of transform.describe?.(parsed.tree, ctx) ?? []) descriptions.set(id, description);
|
|
3049
|
+
}
|
|
2787
3050
|
if (!isMessageTree(tree)) throw errors.X18N2003({ path: candidate.path });
|
|
2788
3051
|
return {
|
|
2789
3052
|
...base,
|
|
2790
3053
|
tree,
|
|
2791
3054
|
rawTree: parsed.tree,
|
|
2792
3055
|
ranges: parsed.ranges ?? /* @__PURE__ */ new Map(),
|
|
3056
|
+
descriptions,
|
|
3057
|
+
states: parsed.states ?? /* @__PURE__ */ new Map(),
|
|
3058
|
+
labels: parsed.labels ?? /* @__PURE__ */ new Map(),
|
|
2793
3059
|
error: void 0
|
|
2794
3060
|
};
|
|
2795
3061
|
} catch (error) {
|
|
@@ -2803,6 +3069,9 @@ function createProject(options) {
|
|
|
2803
3069
|
tree: {},
|
|
2804
3070
|
rawTree: {},
|
|
2805
3071
|
ranges: /* @__PURE__ */ new Map(),
|
|
3072
|
+
descriptions: /* @__PURE__ */ new Map(),
|
|
3073
|
+
states: /* @__PURE__ */ new Map(),
|
|
3074
|
+
labels: /* @__PURE__ */ new Map(),
|
|
2806
3075
|
error: coded
|
|
2807
3076
|
};
|
|
2808
3077
|
}
|
|
@@ -2820,11 +3089,7 @@ function createProject(options) {
|
|
|
2820
3089
|
}
|
|
2821
3090
|
/** A path for a new resource, from the locale finders. */
|
|
2822
3091
|
function newResourcePath(locale, namespace, model) {
|
|
2823
|
-
const ctx =
|
|
2824
|
-
host,
|
|
2825
|
-
config,
|
|
2826
|
-
extensions: [...new Set(registry.formats.flatMap((format) => format.extensions))]
|
|
2827
|
-
};
|
|
3092
|
+
const ctx = finderContext();
|
|
2828
3093
|
for (const finder of registry.finders) {
|
|
2829
3094
|
const path = finder.resolveNew?.({
|
|
2830
3095
|
locale,
|
|
@@ -2834,6 +3099,19 @@ function createProject(options) {
|
|
|
2834
3099
|
if (path) return path;
|
|
2835
3100
|
}
|
|
2836
3101
|
}
|
|
3102
|
+
/** The file of one key, from the locale finders. */
|
|
3103
|
+
function keyFile(key, locale) {
|
|
3104
|
+
const finders = registry.finders.filter((finder) => finder.resolveKey);
|
|
3105
|
+
if (finders.length === 0) return void 0;
|
|
3106
|
+
const ctx = finderContext();
|
|
3107
|
+
for (const finder of finders) {
|
|
3108
|
+
const file = finder.resolveKey?.({
|
|
3109
|
+
key,
|
|
3110
|
+
locale
|
|
3111
|
+
}, ctx);
|
|
3112
|
+
if (file) return file;
|
|
3113
|
+
}
|
|
3114
|
+
}
|
|
2837
3115
|
async function loadResources(candidates, reread) {
|
|
2838
3116
|
const next = /* @__PURE__ */ new Map();
|
|
2839
3117
|
await forEachLimit([...new Set(candidates.map((c) => c.path))], ioConcurrency, async (path) => {
|
|
@@ -2851,7 +3129,7 @@ function createProject(options) {
|
|
|
2851
3129
|
const id = candidate.block ? `${candidate.path}#${candidate.block.index}` : candidate.path;
|
|
2852
3130
|
const previous = resourceStates.get(id);
|
|
2853
3131
|
const content = candidate.block ? text.slice(candidate.block.range.start, candidate.block.range.end) : text;
|
|
2854
|
-
if (previous && previous.hash === hashText(content) && previous.resource.locale === candidate.locale && previous.resource.namespace === candidate.namespace && previous.offset === (candidate.block?.range.start ?? 0)) next.set(id, previous);
|
|
3132
|
+
if (previous && previous.hash === hashText(content) && previous.resource.locale === candidate.locale && previous.resource.namespace === candidate.namespace && previous.resource.keySeparator === candidate.keySeparator && previous.offset === (candidate.block?.range.start ?? 0)) next.set(id, previous);
|
|
2855
3133
|
else next.set(id, parseResource(candidate, text));
|
|
2856
3134
|
}
|
|
2857
3135
|
resourceStates.clear();
|
|
@@ -2963,7 +3241,7 @@ function createProject(options) {
|
|
|
2963
3241
|
usageEligible = (path) => registry.scannersFor(path).length > 0 && include(path) && !exclude(path);
|
|
2964
3242
|
await scanPaths((await host.glob(config.usage.include, {
|
|
2965
3243
|
ignore: config.usage.exclude,
|
|
2966
|
-
gitignore:
|
|
3244
|
+
gitignore: config.usage.gitignore
|
|
2967
3245
|
})).filter((path) => registry.scannersFor(path).length > 0));
|
|
2968
3246
|
}
|
|
2969
3247
|
let index;
|
|
@@ -2985,7 +3263,8 @@ function createProject(options) {
|
|
|
2985
3263
|
const path = [...stored, segment];
|
|
2986
3264
|
const key = keyFor(resource, path);
|
|
2987
3265
|
const storedPath = state.toStored(path);
|
|
2988
|
-
const
|
|
3266
|
+
const id = pathId(storedPath);
|
|
3267
|
+
const raw = state.ranges.get(id);
|
|
2989
3268
|
const range = raw ? {
|
|
2990
3269
|
key: shift(raw.key),
|
|
2991
3270
|
value: shift(raw.value)
|
|
@@ -3002,24 +3281,21 @@ function createProject(options) {
|
|
|
3002
3281
|
messages.set(key, byLocale);
|
|
3003
3282
|
const list = byLocale.get(resource.locale) ?? [];
|
|
3004
3283
|
byLocale.set(resource.locale, list);
|
|
3284
|
+
const description = state.descriptions.get(id);
|
|
3285
|
+
const translation = state.states.get(id);
|
|
3286
|
+
const label = state.labels.get(id);
|
|
3005
3287
|
list.push({
|
|
3006
3288
|
key,
|
|
3007
3289
|
locale: resource.locale,
|
|
3008
3290
|
value: child,
|
|
3009
3291
|
resource: resource.id,
|
|
3010
3292
|
storedPath,
|
|
3011
|
-
range
|
|
3293
|
+
range,
|
|
3294
|
+
...description === void 0 ? {} : { description },
|
|
3295
|
+
...translation === void 0 ? {} : { state: translation },
|
|
3296
|
+
...label === void 0 ? {} : { label }
|
|
3012
3297
|
});
|
|
3013
|
-
if (!keyPaths.has(key))
|
|
3014
|
-
const logical = path.flatMap((s) => s.split("."));
|
|
3015
|
-
keyPaths.set(key, namespaced ? {
|
|
3016
|
-
namespace: resource.namespace ?? config.namespace.default,
|
|
3017
|
-
path: logical
|
|
3018
|
-
} : {
|
|
3019
|
-
namespace: void 0,
|
|
3020
|
-
path: [...resource.namespace ? resource.namespace.split(".") : [], ...logical]
|
|
3021
|
-
});
|
|
3022
|
-
}
|
|
3298
|
+
if (!keyPaths.has(key)) keyPaths.set(key, keyIdFor(resource, path));
|
|
3023
3299
|
} else {
|
|
3024
3300
|
parents.add(key);
|
|
3025
3301
|
visit(child, path);
|
|
@@ -3028,7 +3304,7 @@ function createProject(options) {
|
|
|
3028
3304
|
};
|
|
3029
3305
|
visit(state.tree, []);
|
|
3030
3306
|
}
|
|
3031
|
-
for (const { namespace, path } of keyPaths.values()) for (let i = 1; i < path.length; i++) parents.add(joinKey(namespace, path.slice(0, i)));
|
|
3307
|
+
for (const { namespace, path } of keyPaths.values()) for (let i = 1; i < path.length; i++) parents.add(joinKey(namespace, path.slice(0, i), config.keySeparator));
|
|
3032
3308
|
const derived = /* @__PURE__ */ new Map();
|
|
3033
3309
|
if (config.usage.derivedKeys.length > 0) for (const key of messages.keys()) for (const base of basesOf(key)) {
|
|
3034
3310
|
const list = derived.get(base) ?? [];
|
|
@@ -3116,7 +3392,7 @@ function createProject(options) {
|
|
|
3116
3392
|
}
|
|
3117
3393
|
if (query.prefix !== void 0) {
|
|
3118
3394
|
const prefix = query.prefix;
|
|
3119
|
-
return [...usages.filter((usage) => inPath(usage) && usage.dynamic === false && usage.key !== void 0 && isKeyUnder(usage.key, prefix)).map((usage) => ({
|
|
3395
|
+
return [...usages.filter((usage) => inPath(usage) && usage.dynamic === false && usage.key !== void 0 && isKeyUnder(usage.key, prefix, config.keySeparator)).map((usage) => ({
|
|
3120
3396
|
usage,
|
|
3121
3397
|
possible: false
|
|
3122
3398
|
})), ...prefixUsages.filter((usage) => inPath(usage) && usage.key !== void 0 && (usage.key.startsWith(prefix) || prefix.startsWith(usage.key))).map((usage) => ({
|
|
@@ -3168,7 +3444,7 @@ function createProject(options) {
|
|
|
3168
3444
|
});
|
|
3169
3445
|
function keys(query = {}) {
|
|
3170
3446
|
const { messages, keyPaths } = idx();
|
|
3171
|
-
const match = query.glob ? keyMatcher([query.glob]) : void 0;
|
|
3447
|
+
const match = query.glob ? keyMatcher([query.glob], config.keySeparator) : void 0;
|
|
3172
3448
|
let list = [...messages.keys()];
|
|
3173
3449
|
if (match) list = list.filter((key) => match(key));
|
|
3174
3450
|
if (query.namespace !== void 0) list = list.filter((key) => keyPaths.get(key)?.namespace === query.namespace);
|
|
@@ -3218,6 +3494,7 @@ function createProject(options) {
|
|
|
3218
3494
|
key,
|
|
3219
3495
|
namespace: id.namespace,
|
|
3220
3496
|
path: id.path,
|
|
3497
|
+
label: list.find((m) => m.label !== void 0)?.label,
|
|
3221
3498
|
messages: list,
|
|
3222
3499
|
missing: locales.filter((locale) => !byLocale.has(locale) && needs(key, locale)),
|
|
3223
3500
|
usages: usageMatches({ key }),
|
|
@@ -3370,6 +3647,7 @@ function createProject(options) {
|
|
|
3370
3647
|
basesOf,
|
|
3371
3648
|
toStored: (target, path) => storedPathOf(target)(path),
|
|
3372
3649
|
resolveNew: newResourcePath,
|
|
3650
|
+
resolveKey: keyFile,
|
|
3373
3651
|
update
|
|
3374
3652
|
});
|
|
3375
3653
|
const ready = (async () => {
|
|
@@ -3409,9 +3687,12 @@ function createProject(options) {
|
|
|
3409
3687
|
parseKey(input) {
|
|
3410
3688
|
const separator = config.namespace.separator;
|
|
3411
3689
|
if (!namespaced) return input;
|
|
3412
|
-
|
|
3413
|
-
|
|
3414
|
-
if (
|
|
3690
|
+
const index = input.indexOf(":");
|
|
3691
|
+
if (index > 0 && (separator !== false || [...resourceStates.values()].some((state) => state.resource.namespace === input.slice(0, index)))) return input;
|
|
3692
|
+
if (separator !== ":" && separator !== false) {
|
|
3693
|
+
const custom = input.indexOf(separator);
|
|
3694
|
+
if (custom > 0) return `${input.slice(0, custom)}:${input.slice(custom + separator.length)}`;
|
|
3695
|
+
}
|
|
3415
3696
|
return config.namespace.default ? `${config.namespace.default}:${input}` : input;
|
|
3416
3697
|
},
|
|
3417
3698
|
usages: usageMatches,
|
|
@@ -3453,6 +3734,7 @@ function createProject(options) {
|
|
|
3453
3734
|
},
|
|
3454
3735
|
prompt: (kind, locale, promptOptions = {}) => buildPrompt(project, kind, locale, promptOptions, {
|
|
3455
3736
|
config,
|
|
3737
|
+
fallbacks,
|
|
3456
3738
|
hooks: registry.plugins.flatMap((plugin) => plugin.translatePrompt ? [plugin.translatePrompt] : [])
|
|
3457
3739
|
}),
|
|
3458
3740
|
explain,
|
|
@@ -3460,6 +3742,14 @@ function createProject(options) {
|
|
|
3460
3742
|
text: (path) => fileTexts.get(path) ?? files.get(path)?.text ?? (path === reviewFile ? reviewText : void 0),
|
|
3461
3743
|
owns: (path) => fileTexts.has(path) || files.has(path) || usageEligible(path),
|
|
3462
3744
|
plan: (ops) => planner.plan(ops),
|
|
3745
|
+
planUnused: (query) => planUnusedDeletion({
|
|
3746
|
+
unused: analyzer.analyze().unused,
|
|
3747
|
+
usages: idx().usages,
|
|
3748
|
+
namespaced,
|
|
3749
|
+
ignoreKeys: config.ignoreKeys,
|
|
3750
|
+
locations: (key) => project.locations(key),
|
|
3751
|
+
plan: (ops) => planner.plan(ops)
|
|
3752
|
+
}, query),
|
|
3463
3753
|
apply: (plan, applyOptions) => planner.apply(plan, applyOptions?.answers ?? {}),
|
|
3464
3754
|
update,
|
|
3465
3755
|
watch() {
|
|
@@ -3504,6 +3794,7 @@ function defaultDraft() {
|
|
|
3504
3794
|
resources: [],
|
|
3505
3795
|
formats: {},
|
|
3506
3796
|
keyStyle: "auto",
|
|
3797
|
+
keySeparator: ".",
|
|
3507
3798
|
namespace: {
|
|
3508
3799
|
separator: ".",
|
|
3509
3800
|
default: void 0,
|
|
@@ -3514,6 +3805,7 @@ function defaultDraft() {
|
|
|
3514
3805
|
usage: {
|
|
3515
3806
|
include: ["**/*"],
|
|
3516
3807
|
exclude: defaultExclude,
|
|
3808
|
+
gitignore: true,
|
|
3517
3809
|
calls: [],
|
|
3518
3810
|
regex: [],
|
|
3519
3811
|
ignoreCallees: [],
|
|
@@ -3593,6 +3885,10 @@ function applyLayer(draft, layer, origin, origins) {
|
|
|
3593
3885
|
draft.keyStyle = layer.keyStyle;
|
|
3594
3886
|
mark("keyStyle");
|
|
3595
3887
|
}
|
|
3888
|
+
if (layer.keySeparator !== void 0) {
|
|
3889
|
+
draft.keySeparator = layer.keySeparator;
|
|
3890
|
+
mark("keySeparator");
|
|
3891
|
+
}
|
|
3596
3892
|
if (layer.namespace !== void 0) {
|
|
3597
3893
|
draft.namespace = {
|
|
3598
3894
|
separator: layer.namespace.separator ?? draft.namespace.separator,
|
|
@@ -3611,6 +3907,7 @@ function applyLayer(draft, layer, origin, origins) {
|
|
|
3611
3907
|
draft.usage = {
|
|
3612
3908
|
include: usage.include ?? draft.usage.include,
|
|
3613
3909
|
exclude: [...draft.usage.exclude, ...usage.exclude ?? []],
|
|
3910
|
+
gitignore: usage.gitignore ?? draft.usage.gitignore,
|
|
3614
3911
|
calls: [...draft.usage.calls, ...usage.calls ?? []],
|
|
3615
3912
|
regex: [...draft.usage.regex, ...usage.regex ?? []],
|
|
3616
3913
|
ignoreCallees: [...draft.usage.ignoreCallees, ...usage.ignoreCallees ?? []],
|
|
@@ -3828,4 +4125,4 @@ async function resolveConfig(options) {
|
|
|
3828
4125
|
};
|
|
3829
4126
|
}
|
|
3830
4127
|
//#endregion
|
|
3831
|
-
export { ConfigSchema, PluginFileSchema, ReviewFileSchema, applyMessageOps, applyTextEdits, buildTree, contractVersion, createLibraryPlugin, createLineIndex, createMemoryHost, createOverlayHost, createPlaceholderComparer, createProject, createRegistry, defineConfig, definePlugin, definePreset, diffText, envelope, errors, extensionOf, fuzzyScore, globToRegExp, hashText, isJsonObject, isKeyUnder, isMessageLeaf, isMessageTree, isPreset, isX18nError, joinKey, keyMatcher, localeSpellingKey, parseLocaleInfo, parseMessageBatch, pathId, pathMatcher, placeholderProblems, rangeContains, renderExplanation, renderPrompt, replaceKeyPrefix, resolveConfig, sortTree, splitKey, toErrorInfo, toWireDiagnostic, toWireRange, validate, walkLeaves };
|
|
4128
|
+
export { ConfigSchema, PluginFileSchema, ReviewFileSchema, applyMessageOps, applyTextEdits, buildTree, contractVersion, createLibraryPlugin, createLineIndex, createMemoryHost, createOverlayHost, createPlaceholderComparer, createProject, createRegistry, defineConfig, definePlugin, definePreset, diffText, envelope, errors, extensionOf, fuzzyScore, globToRegExp, hashText, isJsonObject, isKeyUnder, isMessageLeaf, isMessageTree, isPreset, isX18nError, joinKey, keyMatcher, localeSpellingKey, parseLocaleInfo, parseMessageBatch, pathId, pathMatcher, placeholderProblems, rangeContains, relatedLocales, renderExplanation, renderPrompt, replaceKeyPrefix, resolveConfig, sortTree, splitKey, toErrorInfo, toWireDiagnostic, toWireRange, validate, walkLeaves };
|