@x18n/core 0.1.6 → 0.1.7
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 +328 -241
- package/dist/index.mjs +303 -91
- package/package.json +1 -1
- package/schema/config.json +34 -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)
|
|
302
312
|
};
|
|
303
313
|
}
|
|
304
|
-
/**
|
|
305
|
-
function
|
|
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)
|
|
323
|
+
};
|
|
324
|
+
}
|
|
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({
|
|
@@ -857,7 +887,7 @@ const glossaryEntry = v.strictObject({
|
|
|
857
887
|
});
|
|
858
888
|
const checkSetting = v.union([level, v.tuple([level, jsonObject])]);
|
|
859
889
|
const namespaceOptions = v.strictObject({
|
|
860
|
-
separator: v.optional(v.string()),
|
|
890
|
+
separator: v.optional(v.union([v.pipe(v.string(), v.minLength(1)), v.literal(false)])),
|
|
861
891
|
default: v.optional(v.string()),
|
|
862
892
|
fallback: v.optional(strings),
|
|
863
893
|
byFile: v.optional(v.record(v.string(), v.string())),
|
|
@@ -889,6 +919,7 @@ const ConfigSchema = v.strictObject({
|
|
|
889
919
|
"nested",
|
|
890
920
|
"flat"
|
|
891
921
|
])),
|
|
922
|
+
keySeparator: v.optional(keySeparator),
|
|
892
923
|
namespace: v.optional(namespaceOptions),
|
|
893
924
|
usage: v.optional(v.strictObject({
|
|
894
925
|
include: v.optional(strings),
|
|
@@ -937,7 +968,8 @@ const PluginFileSchema = v.strictObject({
|
|
|
937
968
|
"auto",
|
|
938
969
|
"nested",
|
|
939
970
|
"flat"
|
|
940
|
-
]))
|
|
971
|
+
])),
|
|
972
|
+
keySeparator: v.optional(keySeparator)
|
|
941
973
|
})),
|
|
942
974
|
scanners: v.optional(v.strictObject({
|
|
943
975
|
calls: v.optional(v.array(callSpec)),
|
|
@@ -1137,46 +1169,55 @@ function innermost(scopes, range) {
|
|
|
1137
1169
|
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
1170
|
return best;
|
|
1139
1171
|
}
|
|
1172
|
+
function joinPrefix(prefix, text, joiner) {
|
|
1173
|
+
return text ? `${prefix}${joiner}${text}` : prefix;
|
|
1174
|
+
}
|
|
1175
|
+
/** The first prefix whose key exists. A prefix usage (`items.${id}`) ends in an empty segment, so its parent key decides. */
|
|
1176
|
+
function definedPrefix(prefixes, text, joiner, dynamicPrefix, exists) {
|
|
1177
|
+
return prefixes.find((prefix) => {
|
|
1178
|
+
const prefixed = joinPrefix(prefix, text, joiner);
|
|
1179
|
+
return exists(dynamicPrefix && prefixed.endsWith(joiner) ? prefixed.slice(0, -joiner.length) : prefixed);
|
|
1180
|
+
});
|
|
1181
|
+
}
|
|
1140
1182
|
function createUsageResolver(config, namespaced, normalizers) {
|
|
1141
1183
|
const { separator, fallback } = config.namespace;
|
|
1184
|
+
const joiner = keyJoiner(config.keySeparator);
|
|
1185
|
+
const keyOf = (namespace, text) => namespace === void 0 ? text : `${namespace}:${text}`;
|
|
1186
|
+
const normalize = (raw, ctx) => {
|
|
1187
|
+
for (const normalizer of normalizers) {
|
|
1188
|
+
const result = normalizer(raw, ctx);
|
|
1189
|
+
if (result !== void 0) return result;
|
|
1190
|
+
}
|
|
1191
|
+
return raw;
|
|
1192
|
+
};
|
|
1142
1193
|
const byFile = Object.entries(config.namespace.byFile).map(([glob, namespace]) => ({
|
|
1143
1194
|
matches: pathMatcher([glob]),
|
|
1144
1195
|
namespace
|
|
1145
1196
|
}));
|
|
1146
1197
|
return { resolve(candidate, scopes, path, scanner, has) {
|
|
1147
1198
|
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
|
-
}
|
|
1199
|
+
let text = normalize(candidate.raw, { path });
|
|
1157
1200
|
const scopeNamespace = candidate.namespace ?? scope?.namespace ?? byFile.find((rule) => rule.matches(path))?.namespace;
|
|
1158
1201
|
let scopePrefix = candidate.keyPrefix ?? scope?.keyPrefix;
|
|
1159
1202
|
let namespace;
|
|
1160
1203
|
let explicit = false;
|
|
1161
1204
|
if (namespaced) {
|
|
1162
|
-
const index = text.indexOf(separator);
|
|
1205
|
+
const index = separator === false ? -1 : text.indexOf(separator);
|
|
1163
1206
|
explicit = index > 0;
|
|
1164
1207
|
if (explicit) {
|
|
1165
1208
|
namespace = text.slice(0, index);
|
|
1166
|
-
text = text.slice(index + separator.length);
|
|
1209
|
+
text = text.slice(index + (separator || "").length);
|
|
1167
1210
|
} else namespace = scopeNamespace ?? config.namespace.default;
|
|
1168
1211
|
}
|
|
1169
1212
|
if (scopePrefix && !explicit) {
|
|
1170
|
-
const
|
|
1171
|
-
|
|
1172
|
-
if (
|
|
1173
|
-
else scopePrefix = void 0;
|
|
1213
|
+
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;
|
|
1214
|
+
scopePrefix = chosen;
|
|
1215
|
+
if (chosen) text = joinPrefix(chosen, text, joiner);
|
|
1174
1216
|
}
|
|
1175
|
-
const keyPath = text === "" ? [] : text.split(".");
|
|
1176
1217
|
let key;
|
|
1177
1218
|
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) =>
|
|
1219
|
+
else key = keyOf(namespace, text);
|
|
1220
|
+
if (key !== void 0 && namespaced && !explicit && candidate.dynamic === false && !has(key)) key = fallback.map((ns) => keyOf(ns, text)).find(has) ?? key;
|
|
1180
1221
|
return {
|
|
1181
1222
|
key,
|
|
1182
1223
|
raw: candidate.raw,
|
|
@@ -1185,6 +1226,7 @@ function createUsageResolver(config, namespaced, normalizers) {
|
|
|
1185
1226
|
expression: candidate.expression,
|
|
1186
1227
|
...candidate.params ? { params: candidate.params } : {},
|
|
1187
1228
|
...candidate.defaultValue === void 0 ? {} : { defaultValue: candidate.defaultValue },
|
|
1229
|
+
...candidate.description === void 0 ? {} : { description: candidate.description },
|
|
1188
1230
|
scanner,
|
|
1189
1231
|
kind: candidate.kind,
|
|
1190
1232
|
dynamic: candidate.dynamic,
|
|
@@ -1425,8 +1467,8 @@ function runChecks(input) {
|
|
|
1425
1467
|
function createAnalyzer(options) {
|
|
1426
1468
|
const { config, registry } = options;
|
|
1427
1469
|
let memo;
|
|
1428
|
-
const ignored = keyMatcher(config.ignoreKeys);
|
|
1429
|
-
const inUse = keyMatcher(config.usage.keysInUse);
|
|
1470
|
+
const ignored = keyMatcher(config.ignoreKeys, config.keySeparator);
|
|
1471
|
+
const inUse = keyMatcher(config.usage.keysInUse, config.keySeparator);
|
|
1430
1472
|
const placeholderCache = /* @__PURE__ */ new Map();
|
|
1431
1473
|
function analyze() {
|
|
1432
1474
|
const state = options.state();
|
|
@@ -1684,19 +1726,19 @@ function nodeAt(tree, path) {
|
|
|
1684
1726
|
return node;
|
|
1685
1727
|
}
|
|
1686
1728
|
/** True for flat files (`{ "a.b": "x" }`), false for nested ones, undefined when it cannot tell. */
|
|
1687
|
-
function isFlatTree(tree) {
|
|
1729
|
+
function isFlatTree(tree, separator) {
|
|
1688
1730
|
const entries = Object.entries(tree);
|
|
1689
1731
|
if (entries.length === 0) return void 0;
|
|
1690
1732
|
if (entries.some(([, value]) => !isMessageLeaf(value))) return false;
|
|
1691
|
-
return entries.some(([key]) => key.includes(
|
|
1733
|
+
return entries.some(([key]) => key.includes(separator)) ? true : void 0;
|
|
1692
1734
|
}
|
|
1693
1735
|
/** The longest key prefix a tree shares with `path`. */
|
|
1694
|
-
function longestPrefix(tree, prefix, path) {
|
|
1736
|
+
function longestPrefix(tree, prefix, path, separator) {
|
|
1695
1737
|
let best = 0;
|
|
1696
1738
|
for (const [segment, child] of Object.entries(tree)) {
|
|
1697
|
-
const next = [...prefix, ...segment
|
|
1739
|
+
const next = [...prefix, ...splitKeyPath(segment, separator)];
|
|
1698
1740
|
best = Math.max(best, commonPrefix(next, path));
|
|
1699
|
-
if (!isMessageLeaf(child)) best = Math.max(best, longestPrefix(child, next, path));
|
|
1741
|
+
if (!isMessageLeaf(child)) best = Math.max(best, longestPrefix(child, next, path, separator));
|
|
1700
1742
|
}
|
|
1701
1743
|
return best;
|
|
1702
1744
|
}
|
|
@@ -1755,6 +1797,7 @@ var PlanBuilder = class {
|
|
|
1755
1797
|
path: s.resource.path,
|
|
1756
1798
|
locale: s.resource.locale,
|
|
1757
1799
|
namespace: s.resource.namespace,
|
|
1800
|
+
keySeparator: s.resource.keySeparator,
|
|
1758
1801
|
format,
|
|
1759
1802
|
state: s
|
|
1760
1803
|
};
|
|
@@ -1779,24 +1822,36 @@ var PlanBuilder = class {
|
|
|
1779
1822
|
return s;
|
|
1780
1823
|
}
|
|
1781
1824
|
keyId(key) {
|
|
1782
|
-
const id = splitKey(key, this.state.namespaced);
|
|
1825
|
+
const id = splitKey(key, this.state.namespaced, this.config.keySeparator);
|
|
1783
1826
|
return {
|
|
1784
1827
|
namespace: id.namespace,
|
|
1785
1828
|
path: [...id.path]
|
|
1786
1829
|
};
|
|
1787
1830
|
}
|
|
1788
|
-
|
|
1831
|
+
separatorOf(target) {
|
|
1832
|
+
return keySeparatorOf(target, this.config.keySeparator);
|
|
1833
|
+
}
|
|
1834
|
+
/**
|
|
1835
|
+
* Key path relative to a resource, or undefined when the key does not belong there.
|
|
1836
|
+
* A resource of literal keys (`keySeparator: false`) gets the whole rest of the key as one segment.
|
|
1837
|
+
*/
|
|
1789
1838
|
relativePath(key, target) {
|
|
1790
|
-
const { namespace,
|
|
1839
|
+
const { namespace, rest } = splitNamespace(key, this.state.namespaced);
|
|
1840
|
+
const literal = this.separatorOf(target) === false;
|
|
1841
|
+
const joiner = keyJoiner(this.config.keySeparator);
|
|
1842
|
+
const split = (text) => splitKeyPath(text, literal ? false : joiner);
|
|
1791
1843
|
const fallback = this.config.namespace.default;
|
|
1792
|
-
if (this.state.namespaced) return (namespace ?? fallback) === (target.namespace ?? fallback) ?
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1844
|
+
if (this.state.namespaced) return (namespace ?? fallback) === (target.namespace ?? fallback) ? split(rest) : void 0;
|
|
1845
|
+
if (!target.namespace) return rest === "" ? void 0 : split(rest);
|
|
1846
|
+
const prefix = target.namespace.split(".").join(joiner) + joiner;
|
|
1847
|
+
if (rest.length <= prefix.length || !rest.startsWith(prefix)) return void 0;
|
|
1848
|
+
return split(rest.slice(prefix.length));
|
|
1796
1849
|
}
|
|
1797
1850
|
storedPathFor(entry, relative) {
|
|
1798
1851
|
const stored = this.options.toStored(entry.target, relative);
|
|
1799
|
-
|
|
1852
|
+
const separator = this.separatorOf(entry.target);
|
|
1853
|
+
if (separator === false) return stored;
|
|
1854
|
+
return stored.length === relative.length && (isFlatTree(entry.tree, separator) ?? this.config.keyStyle === "flat") ? [stored.join(separator)] : stored;
|
|
1800
1855
|
}
|
|
1801
1856
|
createTarget(key, locale) {
|
|
1802
1857
|
const { resources, sourceLocale, namespaced } = this.state;
|
|
@@ -1816,6 +1871,7 @@ var PlanBuilder = class {
|
|
|
1816
1871
|
path: resolved,
|
|
1817
1872
|
locale,
|
|
1818
1873
|
namespace,
|
|
1874
|
+
keySeparator: model?.resource.keySeparator,
|
|
1819
1875
|
format,
|
|
1820
1876
|
state: void 0
|
|
1821
1877
|
};
|
|
@@ -1835,6 +1891,7 @@ var PlanBuilder = class {
|
|
|
1835
1891
|
path,
|
|
1836
1892
|
locale,
|
|
1837
1893
|
namespace: namespace ?? model.resource.namespace,
|
|
1894
|
+
keySeparator: model.resource.keySeparator,
|
|
1838
1895
|
format,
|
|
1839
1896
|
state: void 0
|
|
1840
1897
|
};
|
|
@@ -1854,7 +1911,7 @@ var PlanBuilder = class {
|
|
|
1854
1911
|
const prefix = s.resource.namespace && !this.state.namespaced ? path.slice(0, s.resource.namespace.split(".").length) : [];
|
|
1855
1912
|
return {
|
|
1856
1913
|
s,
|
|
1857
|
-
best: longestPrefix(s.tree, prefix, path)
|
|
1914
|
+
best: longestPrefix(s.tree, prefix, path, this.separatorOf(s.resource))
|
|
1858
1915
|
};
|
|
1859
1916
|
});
|
|
1860
1917
|
const top = Math.max(...scores.map((x) => x.best));
|
|
@@ -1966,8 +2023,9 @@ var PlanBuilder = class {
|
|
|
1966
2023
|
continue;
|
|
1967
2024
|
}
|
|
1968
2025
|
const entry = this.workFor(this.targetOf(s));
|
|
1969
|
-
const
|
|
1970
|
-
const
|
|
2026
|
+
const separator = this.separatorOf(s.resource);
|
|
2027
|
+
const flat = separator !== false && m.storedPath.length === 1 && m.storedPath[0].includes(separator);
|
|
2028
|
+
const next = flat ? [relative.join(separator)] : this.options.toStored(s.resource, relative);
|
|
1971
2029
|
const sameParent = next.length === m.storedPath.length && next.slice(0, -1).every((segment, i) => segment === m.storedPath[i]);
|
|
1972
2030
|
const last = next.at(-1);
|
|
1973
2031
|
if (sameParent && m.range && (flat ? safeFlat : safeSegment).test(last)) {
|
|
@@ -1999,16 +2057,17 @@ var PlanBuilder = class {
|
|
|
1999
2057
|
const scope = usage.key.slice(0, usage.key.length - usage.raw.length);
|
|
2000
2058
|
if (next.startsWith(`${scope}.`)) return next.slice(scope.length);
|
|
2001
2059
|
}
|
|
2002
|
-
const { namespace,
|
|
2003
|
-
let rest =
|
|
2060
|
+
const { namespace, rest: text } = splitNamespace(next, this.state.namespaced);
|
|
2061
|
+
let rest = text;
|
|
2004
2062
|
const separator = this.config.namespace.separator;
|
|
2005
|
-
const explicit = this.state.namespaced && usage.raw.indexOf(separator) > 0;
|
|
2063
|
+
const explicit = this.state.namespaced && separator !== false && usage.raw.indexOf(separator) > 0;
|
|
2006
2064
|
if (usage.scopePrefix && !explicit) {
|
|
2007
|
-
|
|
2008
|
-
|
|
2065
|
+
const prefix = usage.scopePrefix + keyJoiner(this.config.keySeparator);
|
|
2066
|
+
if (!rest.startsWith(prefix)) return void 0;
|
|
2067
|
+
rest = rest.slice(prefix.length);
|
|
2009
2068
|
}
|
|
2010
2069
|
if (!this.state.namespaced) return rest;
|
|
2011
|
-
if (explicit || namespace !== (usage.scopeNamespace ?? this.config.namespace.default)) return `${namespace}${separator}${rest}`;
|
|
2070
|
+
if (explicit || namespace !== (usage.scopeNamespace ?? this.config.namespace.default)) return separator === false ? void 0 : `${namespace}${separator}${rest}`;
|
|
2012
2071
|
return rest;
|
|
2013
2072
|
}
|
|
2014
2073
|
editSource(path, edit) {
|
|
@@ -2053,7 +2112,7 @@ var PlanBuilder = class {
|
|
|
2053
2112
|
if (op.from === op.to) return;
|
|
2054
2113
|
const all = this.options.allKeys();
|
|
2055
2114
|
const isLeaf = this.options.messages(op.from) !== void 0;
|
|
2056
|
-
const under = all.filter((key) => isKeyUnder(key, op.from));
|
|
2115
|
+
const under = all.filter((key) => isKeyUnder(key, op.from, this.config.keySeparator));
|
|
2057
2116
|
const derived = all.filter((key) => this.options.basesOf(key).includes(op.from));
|
|
2058
2117
|
let keys;
|
|
2059
2118
|
if (op.type === "rename-prefix") keys = under;
|
|
@@ -2073,7 +2132,7 @@ var PlanBuilder = class {
|
|
|
2073
2132
|
this.rewritePrefixUsages(op.from, op.to);
|
|
2074
2133
|
}
|
|
2075
2134
|
delete(op) {
|
|
2076
|
-
const keys = this.options.messages(op.key) ? [op.key] : this.options.allKeys().filter((key) => isKeyUnder(key, op.key));
|
|
2135
|
+
const keys = this.options.messages(op.key) ? [op.key] : this.options.allKeys().filter((key) => isKeyUnder(key, op.key, this.config.keySeparator));
|
|
2077
2136
|
if (keys.length === 0) throw errors.X18N4006({ key: op.key });
|
|
2078
2137
|
for (const key of keys) {
|
|
2079
2138
|
this.touched.add(key);
|
|
@@ -2417,6 +2476,10 @@ function createPlanner(options) {
|
|
|
2417
2476
|
}
|
|
2418
2477
|
//#endregion
|
|
2419
2478
|
//#region src/project/prompt.ts
|
|
2479
|
+
/** Related locales shown per message, at most. */
|
|
2480
|
+
const RELATED_LIMIT = 2;
|
|
2481
|
+
/** Characters of a description or a related value, at most; longer text is cut. */
|
|
2482
|
+
const TEXT_LIMIT = 300;
|
|
2420
2483
|
function text(value) {
|
|
2421
2484
|
return value === void 0 || value === null ? void 0 : String(value);
|
|
2422
2485
|
}
|
|
@@ -2451,10 +2514,36 @@ function reportedValues(project, diagnostics, to, check) {
|
|
|
2451
2514
|
}
|
|
2452
2515
|
return reported;
|
|
2453
2516
|
}
|
|
2517
|
+
function clip(value) {
|
|
2518
|
+
return value.length > TEXT_LIMIT ? `${value.slice(0, 299)}…` : value;
|
|
2519
|
+
}
|
|
2520
|
+
const subtagsOf = (code) => code.toLowerCase().split(/[-_]/);
|
|
2521
|
+
/**
|
|
2522
|
+
* The locales whose values help translate to `to`, nearest first: the fallback chain of `to`, then the other locales of
|
|
2523
|
+
* the same language, those that share more subtags with `to` first, then the shorter codes (`pt` and `pt-PT` for `pt-BR`,
|
|
2524
|
+
* `pt-BR` for `pt`). The source locale and `to` itself are never related.
|
|
2525
|
+
*/
|
|
2526
|
+
function relatedLocales(to, locales, sourceLocale, fallbacks) {
|
|
2527
|
+
const target = subtagsOf(to);
|
|
2528
|
+
const shared = (code) => {
|
|
2529
|
+
const tags = subtagsOf(code);
|
|
2530
|
+
let n = 0;
|
|
2531
|
+
while (n < tags.length && tags[n] === target[n]) n++;
|
|
2532
|
+
return n;
|
|
2533
|
+
};
|
|
2534
|
+
const chain = fallbackChain(fallbacks, to).filter((code) => code !== sourceLocale && code !== to);
|
|
2535
|
+
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));
|
|
2536
|
+
return [...chain, ...language];
|
|
2537
|
+
}
|
|
2538
|
+
/** The description of a key: from the source resource, else another locale's, else a usage's. */
|
|
2539
|
+
function descriptionOf(project, key, from) {
|
|
2540
|
+
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();
|
|
2541
|
+
return trimmed ? clip(trimmed) : void 0;
|
|
2542
|
+
}
|
|
2454
2543
|
/** Builds the parts of a translate or review prompt from project state. */
|
|
2455
2544
|
function buildPrompt(project, kind, to, options, extra) {
|
|
2456
2545
|
const from = project.sourceLocale;
|
|
2457
|
-
const match = typeof options.keys === "string" ? keyMatcher([options.keys]) : void 0;
|
|
2546
|
+
const match = typeof options.keys === "string" ? keyMatcher([options.keys], extra.config.keySeparator) : void 0;
|
|
2458
2547
|
const list = Array.isArray(options.keys) ? new Set(options.keys) : void 0;
|
|
2459
2548
|
const analysis = project.analyze();
|
|
2460
2549
|
const outdated = new Set(analysis.coverage.find((c) => c.locale === to)?.outdated);
|
|
@@ -2475,12 +2564,24 @@ function buildPrompt(project, kind, to, options, extra) {
|
|
|
2475
2564
|
const end = start + page.length;
|
|
2476
2565
|
const syntaxes = project.placeholderSyntaxes;
|
|
2477
2566
|
const info = project.localeInfo(to);
|
|
2567
|
+
const related = options.related === false ? [] : Array.isArray(options.related) ? options.related.filter((code) => code !== to && code !== from) : relatedLocales(to, project.locales, from, extra.fallbacks);
|
|
2568
|
+
const relatedOf = (key, source) => related.flatMap((locale) => {
|
|
2569
|
+
const value = text(project.message(key, locale)?.value);
|
|
2570
|
+
return value && value !== source ? [{
|
|
2571
|
+
locale,
|
|
2572
|
+
value: clip(value)
|
|
2573
|
+
}] : [];
|
|
2574
|
+
}).slice(0, RELATED_LIMIT);
|
|
2575
|
+
const relatedValues = new Map(page.map((key) => [key, relatedOf(key, text(project.message(key, from)?.value) ?? "")]));
|
|
2576
|
+
const descriptions = new Map(page.map((key) => [key, descriptionOf(project, key, from)]));
|
|
2478
2577
|
const draft = {
|
|
2479
2578
|
rules: [],
|
|
2480
2579
|
notes: []
|
|
2481
2580
|
};
|
|
2482
2581
|
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
2582
|
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\".");
|
|
2583
|
+
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.");
|
|
2584
|
+
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
2585
|
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
2586
|
if (info.pluralCategories.length > 1) draft.notes.push(`${to} uses the plural categories ${info.pluralCategories.join(", ")}.`);
|
|
2486
2587
|
for (const hook of extra.hooks) hook(draft, {
|
|
@@ -2501,10 +2602,13 @@ function buildPrompt(project, kind, to, options, extra) {
|
|
|
2501
2602
|
const source = text(project.message(key, from)?.value) ?? "";
|
|
2502
2603
|
const translated = project.history(key, to);
|
|
2503
2604
|
const previous = translated && stale(key) ? text(project.valueAt(key, from, translated.updatedAt)) : void 0;
|
|
2605
|
+
const values = relatedValues.get(key) ?? [];
|
|
2504
2606
|
return {
|
|
2505
2607
|
key,
|
|
2506
2608
|
source,
|
|
2507
2609
|
placeholders: [...new Set(syntaxes.flatMap((s) => s.find(source)).map((p) => p.text))],
|
|
2610
|
+
description: descriptions.get(key),
|
|
2611
|
+
related: values.length > 0 ? values : void 0,
|
|
2508
2612
|
existing: text(project.message(key, to)?.value) || void 0,
|
|
2509
2613
|
previousSource: previous === source ? void 0 : previous,
|
|
2510
2614
|
issues: reported.get(key)?.map((d) => ({
|
|
@@ -2553,6 +2657,8 @@ function renderItem(item, to) {
|
|
|
2553
2657
|
`- Source: ${JSON.stringify(item.source)}`
|
|
2554
2658
|
];
|
|
2555
2659
|
if (item.placeholders.length) out.push(`- Placeholders: ${item.placeholders.map((p) => `\`${p}\``).join(", ")}`);
|
|
2660
|
+
if (item.description !== void 0) out.push(`- Description: ${JSON.stringify(item.description)}`);
|
|
2661
|
+
for (const value of item.related ?? []) out.push(`- In ${value.locale}: ${JSON.stringify(value.value)}`);
|
|
2556
2662
|
if (item.previousSource !== void 0) out.push(`- Source when ${to} last changed: ${JSON.stringify(item.previousSource)}`);
|
|
2557
2663
|
if (item.existing !== void 0) out.push(`- Current ${to}: ${JSON.stringify(item.existing)}`);
|
|
2558
2664
|
for (const issue of item.issues ?? []) out.push(`- Reported: ${issue.message} (\`${issue.code}\`)`);
|
|
@@ -2584,6 +2690,80 @@ function renderPrompt(data) {
|
|
|
2584
2690
|
return out.join("\n");
|
|
2585
2691
|
}
|
|
2586
2692
|
//#endregion
|
|
2693
|
+
//#region src/project/unused.ts
|
|
2694
|
+
/**
|
|
2695
|
+
* True when a fully dynamic usage (`t(key)`) may reach `key`: the key lies in the usage's scope namespace and under its
|
|
2696
|
+
* scope key prefix. Undefined for a usage without a scope, which may reach any key.
|
|
2697
|
+
*/
|
|
2698
|
+
function scopeMatcher(usage, namespaced) {
|
|
2699
|
+
const namespace = namespaced ? usage.scopeNamespace : void 0;
|
|
2700
|
+
const prefix = usage.scopePrefix;
|
|
2701
|
+
if (namespace === void 0 && !prefix) return void 0;
|
|
2702
|
+
return (key) => {
|
|
2703
|
+
const id = splitKey(key, namespaced);
|
|
2704
|
+
if (namespace !== void 0 && id.namespace !== namespace) return false;
|
|
2705
|
+
if (!prefix) return true;
|
|
2706
|
+
const path = id.path.join(".");
|
|
2707
|
+
return path === prefix || path.startsWith(`${prefix}.`);
|
|
2708
|
+
};
|
|
2709
|
+
}
|
|
2710
|
+
function planUnusedDeletion(options, query = {}) {
|
|
2711
|
+
const ignored = keyMatcher(options.ignoreKeys);
|
|
2712
|
+
const match = query.glob ? keyMatcher([query.glob]) : void 0;
|
|
2713
|
+
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();
|
|
2714
|
+
const scoped = [];
|
|
2715
|
+
const unscoped = [];
|
|
2716
|
+
for (const usage of options.usages) {
|
|
2717
|
+
if (usage.dynamic !== "unknown") continue;
|
|
2718
|
+
const reaches = scopeMatcher(usage, options.namespaced);
|
|
2719
|
+
if (reaches) scoped.push({
|
|
2720
|
+
usage,
|
|
2721
|
+
reaches
|
|
2722
|
+
});
|
|
2723
|
+
else unscoped.push(usage);
|
|
2724
|
+
}
|
|
2725
|
+
const keys = [];
|
|
2726
|
+
const kept = [];
|
|
2727
|
+
for (const key of candidates) {
|
|
2728
|
+
const usages = scoped.filter((s) => s.reaches(key)).map((s) => s.usage);
|
|
2729
|
+
if (usages.length > 0) {
|
|
2730
|
+
kept.push({
|
|
2731
|
+
key,
|
|
2732
|
+
usages
|
|
2733
|
+
});
|
|
2734
|
+
continue;
|
|
2735
|
+
}
|
|
2736
|
+
keys.push({
|
|
2737
|
+
key,
|
|
2738
|
+
messages: options.locations(key).filter((node) => node.leaf).map((node) => ({
|
|
2739
|
+
locale: node.locale,
|
|
2740
|
+
path: node.path
|
|
2741
|
+
}))
|
|
2742
|
+
});
|
|
2743
|
+
}
|
|
2744
|
+
const plan = options.plan(keys.map(({ key }) => ({
|
|
2745
|
+
type: "delete",
|
|
2746
|
+
key,
|
|
2747
|
+
locales: "all"
|
|
2748
|
+
})));
|
|
2749
|
+
if (unscoped.length === 0 || keys.length === 0) return {
|
|
2750
|
+
plan,
|
|
2751
|
+
keys,
|
|
2752
|
+
kept
|
|
2753
|
+
};
|
|
2754
|
+
const files = [...new Set(unscoped.map((usage) => usage.path))].sort();
|
|
2755
|
+
const shown = files.length > 5 ? `${files.slice(0, 5).join(", ")} and ${files.length - 5} more files` : files.join(", ");
|
|
2756
|
+
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.`;
|
|
2757
|
+
return {
|
|
2758
|
+
plan: {
|
|
2759
|
+
...plan,
|
|
2760
|
+
warnings: [...plan.warnings, warning]
|
|
2761
|
+
},
|
|
2762
|
+
keys,
|
|
2763
|
+
kept
|
|
2764
|
+
};
|
|
2765
|
+
}
|
|
2766
|
+
//#endregion
|
|
2587
2767
|
//#region src/project/project.ts
|
|
2588
2768
|
const ioConcurrency = 64;
|
|
2589
2769
|
async function forEachLimit(items, limit, fn) {
|
|
@@ -2725,7 +2905,8 @@ function createProject(options) {
|
|
|
2725
2905
|
format: candidate.format,
|
|
2726
2906
|
block: candidate.block,
|
|
2727
2907
|
readonly: candidate.readonly === true,
|
|
2728
|
-
finder: finder.id
|
|
2908
|
+
finder: finder.id,
|
|
2909
|
+
keySeparator: candidate.keySeparator === config.keySeparator ? void 0 : candidate.keySeparator
|
|
2729
2910
|
});
|
|
2730
2911
|
}
|
|
2731
2912
|
finderResults.push({
|
|
@@ -2735,10 +2916,22 @@ function createProject(options) {
|
|
|
2735
2916
|
}
|
|
2736
2917
|
return out;
|
|
2737
2918
|
}
|
|
2919
|
+
/** Namespace and key path of a stored path: each stored segment is split by the resource's key separator. */
|
|
2920
|
+
function keyIdFor(resource, stored) {
|
|
2921
|
+
const separator = keySeparatorOf(resource, config.keySeparator);
|
|
2922
|
+
const logical = stored.flatMap((segment) => splitKeyPath(segment, separator));
|
|
2923
|
+
if (namespaced) return {
|
|
2924
|
+
namespace: resource.namespace ?? config.namespace.default,
|
|
2925
|
+
path: logical
|
|
2926
|
+
};
|
|
2927
|
+
return {
|
|
2928
|
+
namespace: void 0,
|
|
2929
|
+
path: [...resource.namespace ? resource.namespace.split(".") : [], ...logical]
|
|
2930
|
+
};
|
|
2931
|
+
}
|
|
2738
2932
|
function keyFor(resource, stored) {
|
|
2739
|
-
const
|
|
2740
|
-
|
|
2741
|
-
return joinKey(void 0, [...resource.namespace ? resource.namespace.split(".") : [], ...logical]);
|
|
2933
|
+
const { namespace, path } = keyIdFor(resource, stored);
|
|
2934
|
+
return joinKey(namespace, path, config.keySeparator);
|
|
2742
2935
|
}
|
|
2743
2936
|
/** Key path to stored path through the transforms that apply to a resource, last first. */
|
|
2744
2937
|
function storedPathOf(ctx) {
|
|
@@ -2756,7 +2949,8 @@ function createProject(options) {
|
|
|
2756
2949
|
format: format?.id ?? "unknown",
|
|
2757
2950
|
block: candidate.block,
|
|
2758
2951
|
readonly: candidate.readonly || !format || !format.edit && !format.serialize,
|
|
2759
|
-
finder: candidate.finder
|
|
2952
|
+
finder: candidate.finder,
|
|
2953
|
+
...candidate.keySeparator === void 0 ? {} : { keySeparator: candidate.keySeparator }
|
|
2760
2954
|
};
|
|
2761
2955
|
const content = candidate.block ? text.slice(candidate.block.range.start, candidate.block.range.end) : text;
|
|
2762
2956
|
const offset = candidate.block?.range.start ?? 0;
|
|
@@ -2778,18 +2972,25 @@ function createProject(options) {
|
|
|
2778
2972
|
tree: {},
|
|
2779
2973
|
rawTree: {},
|
|
2780
2974
|
ranges: /* @__PURE__ */ new Map(),
|
|
2975
|
+
descriptions: /* @__PURE__ */ new Map(),
|
|
2781
2976
|
error: errors.X18N2001({ path: candidate.path })
|
|
2782
2977
|
};
|
|
2783
2978
|
try {
|
|
2784
2979
|
const parsed = format.parse(content, ctx);
|
|
2785
2980
|
let tree = parsed.tree;
|
|
2786
|
-
|
|
2981
|
+
const descriptions = new Map(parsed.descriptions);
|
|
2982
|
+
for (const transform of transforms) {
|
|
2983
|
+
if (transform.applies?.(ctx) === false) continue;
|
|
2984
|
+
tree = transform.read(tree, ctx);
|
|
2985
|
+
for (const [id, description] of transform.describe?.(parsed.tree, ctx) ?? []) descriptions.set(id, description);
|
|
2986
|
+
}
|
|
2787
2987
|
if (!isMessageTree(tree)) throw errors.X18N2003({ path: candidate.path });
|
|
2788
2988
|
return {
|
|
2789
2989
|
...base,
|
|
2790
2990
|
tree,
|
|
2791
2991
|
rawTree: parsed.tree,
|
|
2792
2992
|
ranges: parsed.ranges ?? /* @__PURE__ */ new Map(),
|
|
2993
|
+
descriptions,
|
|
2793
2994
|
error: void 0
|
|
2794
2995
|
};
|
|
2795
2996
|
} catch (error) {
|
|
@@ -2803,6 +3004,7 @@ function createProject(options) {
|
|
|
2803
3004
|
tree: {},
|
|
2804
3005
|
rawTree: {},
|
|
2805
3006
|
ranges: /* @__PURE__ */ new Map(),
|
|
3007
|
+
descriptions: /* @__PURE__ */ new Map(),
|
|
2806
3008
|
error: coded
|
|
2807
3009
|
};
|
|
2808
3010
|
}
|
|
@@ -2851,7 +3053,7 @@ function createProject(options) {
|
|
|
2851
3053
|
const id = candidate.block ? `${candidate.path}#${candidate.block.index}` : candidate.path;
|
|
2852
3054
|
const previous = resourceStates.get(id);
|
|
2853
3055
|
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);
|
|
3056
|
+
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
3057
|
else next.set(id, parseResource(candidate, text));
|
|
2856
3058
|
}
|
|
2857
3059
|
resourceStates.clear();
|
|
@@ -3002,24 +3204,17 @@ function createProject(options) {
|
|
|
3002
3204
|
messages.set(key, byLocale);
|
|
3003
3205
|
const list = byLocale.get(resource.locale) ?? [];
|
|
3004
3206
|
byLocale.set(resource.locale, list);
|
|
3207
|
+
const description = state.descriptions.get(pathId(storedPath));
|
|
3005
3208
|
list.push({
|
|
3006
3209
|
key,
|
|
3007
3210
|
locale: resource.locale,
|
|
3008
3211
|
value: child,
|
|
3009
3212
|
resource: resource.id,
|
|
3010
3213
|
storedPath,
|
|
3011
|
-
range
|
|
3214
|
+
range,
|
|
3215
|
+
...description === void 0 ? {} : { description }
|
|
3012
3216
|
});
|
|
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
|
-
}
|
|
3217
|
+
if (!keyPaths.has(key)) keyPaths.set(key, keyIdFor(resource, path));
|
|
3023
3218
|
} else {
|
|
3024
3219
|
parents.add(key);
|
|
3025
3220
|
visit(child, path);
|
|
@@ -3028,7 +3223,7 @@ function createProject(options) {
|
|
|
3028
3223
|
};
|
|
3029
3224
|
visit(state.tree, []);
|
|
3030
3225
|
}
|
|
3031
|
-
for (const { namespace, path } of keyPaths.values()) for (let i = 1; i < path.length; i++) parents.add(joinKey(namespace, path.slice(0, i)));
|
|
3226
|
+
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
3227
|
const derived = /* @__PURE__ */ new Map();
|
|
3033
3228
|
if (config.usage.derivedKeys.length > 0) for (const key of messages.keys()) for (const base of basesOf(key)) {
|
|
3034
3229
|
const list = derived.get(base) ?? [];
|
|
@@ -3116,7 +3311,7 @@ function createProject(options) {
|
|
|
3116
3311
|
}
|
|
3117
3312
|
if (query.prefix !== void 0) {
|
|
3118
3313
|
const prefix = query.prefix;
|
|
3119
|
-
return [...usages.filter((usage) => inPath(usage) && usage.dynamic === false && usage.key !== void 0 && isKeyUnder(usage.key, prefix)).map((usage) => ({
|
|
3314
|
+
return [...usages.filter((usage) => inPath(usage) && usage.dynamic === false && usage.key !== void 0 && isKeyUnder(usage.key, prefix, config.keySeparator)).map((usage) => ({
|
|
3120
3315
|
usage,
|
|
3121
3316
|
possible: false
|
|
3122
3317
|
})), ...prefixUsages.filter((usage) => inPath(usage) && usage.key !== void 0 && (usage.key.startsWith(prefix) || prefix.startsWith(usage.key))).map((usage) => ({
|
|
@@ -3168,7 +3363,7 @@ function createProject(options) {
|
|
|
3168
3363
|
});
|
|
3169
3364
|
function keys(query = {}) {
|
|
3170
3365
|
const { messages, keyPaths } = idx();
|
|
3171
|
-
const match = query.glob ? keyMatcher([query.glob]) : void 0;
|
|
3366
|
+
const match = query.glob ? keyMatcher([query.glob], config.keySeparator) : void 0;
|
|
3172
3367
|
let list = [...messages.keys()];
|
|
3173
3368
|
if (match) list = list.filter((key) => match(key));
|
|
3174
3369
|
if (query.namespace !== void 0) list = list.filter((key) => keyPaths.get(key)?.namespace === query.namespace);
|
|
@@ -3409,9 +3604,12 @@ function createProject(options) {
|
|
|
3409
3604
|
parseKey(input) {
|
|
3410
3605
|
const separator = config.namespace.separator;
|
|
3411
3606
|
if (!namespaced) return input;
|
|
3412
|
-
|
|
3413
|
-
|
|
3414
|
-
if (
|
|
3607
|
+
const index = input.indexOf(":");
|
|
3608
|
+
if (index > 0 && (separator !== false || [...resourceStates.values()].some((state) => state.resource.namespace === input.slice(0, index)))) return input;
|
|
3609
|
+
if (separator !== ":" && separator !== false) {
|
|
3610
|
+
const custom = input.indexOf(separator);
|
|
3611
|
+
if (custom > 0) return `${input.slice(0, custom)}:${input.slice(custom + separator.length)}`;
|
|
3612
|
+
}
|
|
3415
3613
|
return config.namespace.default ? `${config.namespace.default}:${input}` : input;
|
|
3416
3614
|
},
|
|
3417
3615
|
usages: usageMatches,
|
|
@@ -3453,6 +3651,7 @@ function createProject(options) {
|
|
|
3453
3651
|
},
|
|
3454
3652
|
prompt: (kind, locale, promptOptions = {}) => buildPrompt(project, kind, locale, promptOptions, {
|
|
3455
3653
|
config,
|
|
3654
|
+
fallbacks,
|
|
3456
3655
|
hooks: registry.plugins.flatMap((plugin) => plugin.translatePrompt ? [plugin.translatePrompt] : [])
|
|
3457
3656
|
}),
|
|
3458
3657
|
explain,
|
|
@@ -3460,6 +3659,14 @@ function createProject(options) {
|
|
|
3460
3659
|
text: (path) => fileTexts.get(path) ?? files.get(path)?.text ?? (path === reviewFile ? reviewText : void 0),
|
|
3461
3660
|
owns: (path) => fileTexts.has(path) || files.has(path) || usageEligible(path),
|
|
3462
3661
|
plan: (ops) => planner.plan(ops),
|
|
3662
|
+
planUnused: (query) => planUnusedDeletion({
|
|
3663
|
+
unused: analyzer.analyze().unused,
|
|
3664
|
+
usages: idx().usages,
|
|
3665
|
+
namespaced,
|
|
3666
|
+
ignoreKeys: config.ignoreKeys,
|
|
3667
|
+
locations: (key) => project.locations(key),
|
|
3668
|
+
plan: (ops) => planner.plan(ops)
|
|
3669
|
+
}, query),
|
|
3463
3670
|
apply: (plan, applyOptions) => planner.apply(plan, applyOptions?.answers ?? {}),
|
|
3464
3671
|
update,
|
|
3465
3672
|
watch() {
|
|
@@ -3504,6 +3711,7 @@ function defaultDraft() {
|
|
|
3504
3711
|
resources: [],
|
|
3505
3712
|
formats: {},
|
|
3506
3713
|
keyStyle: "auto",
|
|
3714
|
+
keySeparator: ".",
|
|
3507
3715
|
namespace: {
|
|
3508
3716
|
separator: ".",
|
|
3509
3717
|
default: void 0,
|
|
@@ -3593,6 +3801,10 @@ function applyLayer(draft, layer, origin, origins) {
|
|
|
3593
3801
|
draft.keyStyle = layer.keyStyle;
|
|
3594
3802
|
mark("keyStyle");
|
|
3595
3803
|
}
|
|
3804
|
+
if (layer.keySeparator !== void 0) {
|
|
3805
|
+
draft.keySeparator = layer.keySeparator;
|
|
3806
|
+
mark("keySeparator");
|
|
3807
|
+
}
|
|
3596
3808
|
if (layer.namespace !== void 0) {
|
|
3597
3809
|
draft.namespace = {
|
|
3598
3810
|
separator: layer.namespace.separator ?? draft.namespace.separator,
|
|
@@ -3828,4 +4040,4 @@ async function resolveConfig(options) {
|
|
|
3828
4040
|
};
|
|
3829
4041
|
}
|
|
3830
4042
|
//#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 };
|
|
4043
|
+
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 };
|