@x18n/core 0.0.12 → 0.0.13
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 +21 -4
- package/dist/index.mjs +187 -113
- package/package.json +1 -1
- package/schema/config.json +6 -0
package/dist/index.d.mts
CHANGED
|
@@ -196,6 +196,8 @@ interface LocaleInfo {
|
|
|
196
196
|
readonly displayName: string | undefined;
|
|
197
197
|
readonly flag: string | undefined;
|
|
198
198
|
readonly pluralCategories: readonly Intl.LDMLPluralRule[];
|
|
199
|
+
/** Categories of ordinal numbers (1st, 2nd), which differ from the cardinal ones. */
|
|
200
|
+
readonly ordinalCategories: readonly Intl.LDMLPluralRule[];
|
|
199
201
|
}
|
|
200
202
|
/** Embedded resource inside a file, for example a Vue `<i18n>` block. */
|
|
201
203
|
interface ResourceBlock {
|
|
@@ -237,6 +239,8 @@ interface Usage {
|
|
|
237
239
|
readonly path: string;
|
|
238
240
|
/** Range of the key text only (inside the quotes): the range rename rewrites. */
|
|
239
241
|
readonly range: TextRange;
|
|
242
|
+
/** Range of the whole call that holds the key (`t('a', { n })`), when the scanner knows it. */
|
|
243
|
+
readonly expression: TextRange | undefined;
|
|
240
244
|
readonly scanner: string;
|
|
241
245
|
readonly kind: UsageKind;
|
|
242
246
|
/** `t(`pages.${id}.title`)` gives 'prefix' with key `pages`. */
|
|
@@ -479,6 +483,8 @@ interface ScanContext {
|
|
|
479
483
|
interface UsageCandidate {
|
|
480
484
|
readonly raw: string;
|
|
481
485
|
readonly range: TextRange;
|
|
486
|
+
/** Range of the whole call that holds the key, when the scanner knows it. */
|
|
487
|
+
readonly expression?: TextRange | undefined;
|
|
482
488
|
readonly kind: UsageKind;
|
|
483
489
|
readonly dynamic: false | "prefix" | "unknown";
|
|
484
490
|
readonly namespace?: string | undefined;
|
|
@@ -571,6 +577,13 @@ interface HistoryProvider {
|
|
|
571
577
|
/** Calls `onChange` when the history changes outside resource files, for example on a commit. */
|
|
572
578
|
readonly watch?: ((ctx: HistoryContext, onChange: () => void) => Disposable) | undefined;
|
|
573
579
|
}
|
|
580
|
+
/** A key that is one plural form of a base key, such as `item_one` of `item`. */
|
|
581
|
+
interface PluralForm {
|
|
582
|
+
readonly type: Intl.PluralRuleType;
|
|
583
|
+
readonly category: Intl.LDMLPluralRule;
|
|
584
|
+
/** The sibling key of another category. */
|
|
585
|
+
readonly keyOf: (category: Intl.LDMLPluralRule) => KeyString;
|
|
586
|
+
}
|
|
574
587
|
interface CheckContext {
|
|
575
588
|
readonly config: ResolvedConfig;
|
|
576
589
|
readonly sourceLocale: LocaleCode;
|
|
@@ -587,6 +600,8 @@ interface CheckContext {
|
|
|
587
600
|
readonly message: (key: KeyString, locale: LocaleCode) => Message | undefined;
|
|
588
601
|
/** Every resource message of a key in a locale, more than one when duplicated across files. */
|
|
589
602
|
readonly duplicates: (key: KeyString, locale: LocaleCode) => readonly Message[];
|
|
603
|
+
/** The plural form a key is, from the `{plural}` and `{ordinal}` derived-key rules. */
|
|
604
|
+
readonly formOf: (key: KeyString) => PluralForm | undefined;
|
|
590
605
|
readonly usages: () => Iterable<Usage>;
|
|
591
606
|
readonly coverage: () => readonly LocaleCoverage[];
|
|
592
607
|
readonly unused: () => readonly KeyString[];
|
|
@@ -785,12 +800,14 @@ interface ExtractOptions {
|
|
|
785
800
|
readonly keyMaxLength?: number | undefined;
|
|
786
801
|
/** Reuse a key when the same text exists as a source value. Default true. */
|
|
787
802
|
readonly reuseExisting?: boolean | undefined;
|
|
788
|
-
/** Strings or `/regex
|
|
803
|
+
/** Strings never extracted: an exact string such as `Elk`, or a `/regex/flags` string such as `/^v\d+/i` that matches the text. */
|
|
789
804
|
readonly ignore?: readonly string[] | undefined;
|
|
790
|
-
/**
|
|
805
|
+
/** Strings never extracted in the files of a glob, written as in `ignore`. */
|
|
791
806
|
readonly ignoreByFile?: {
|
|
792
807
|
readonly [glob: string]: readonly string[];
|
|
793
808
|
} | undefined;
|
|
809
|
+
/** Attribute names whose values are text, on top of those whose last word is `title`, `label`, `text`, `placeholder`, `alt`, `content`, `description`, `message` and the like. Markup attributes outside this set are never extracted. */
|
|
810
|
+
readonly attributes?: readonly string[] | undefined;
|
|
794
811
|
readonly ignoreAttributes?: readonly string[] | undefined;
|
|
795
812
|
readonly ignoreCallees?: readonly string[] | undefined;
|
|
796
813
|
readonly templates?: readonly (string | ExtractTemplate)[] | undefined;
|
|
@@ -804,7 +821,7 @@ interface UsageOptions {
|
|
|
804
821
|
readonly ignoreCallees?: readonly string[] | undefined;
|
|
805
822
|
/** Keys (globs) that count as used, for example keys built at runtime. */
|
|
806
823
|
readonly keysInUse?: readonly string[] | undefined;
|
|
807
|
-
/** Rules like `{key}_plural`: when `{key}` is used the derived keys count as used. */
|
|
824
|
+
/** Rules like `{key}_plural`: when `{key}` is used the derived keys count as used. Rules apply repeatedly, so with `{key}_*` the key `a_b_c` derives from both `a_b` and `a`. `*` matches one key segment. `{plural}` and `{ordinal}` match a plural or ordinal category (`one`, `few`, `other`): each locale then needs the categories of its own language, and categories the language lacks are optional. */
|
|
808
825
|
readonly derivedKeys?: readonly string[] | undefined;
|
|
809
826
|
}
|
|
810
827
|
interface WriteOptions {
|
|
@@ -1456,4 +1473,4 @@ export declare function rangeContains(range: TextRange, offset: number): boolean
|
|
|
1456
1473
|
/** Fast non-cryptographic string hash (cyrb53), as hex. */
|
|
1457
1474
|
export declare function hashText(text: string): string;
|
|
1458
1475
|
//#endregion
|
|
1459
|
-
export type { Analysis, Answers, ApplyResult, ArgSource, Builtins, CacheStore, CallSpec, CapabilityConflict, Check, CheckContext, CheckIds, CheckLevel, CheckSetting, ConfigContext, ConfigDraft, ConfigOrigin, CreateProjectOptions, Decision, DecisionOption, DetectContext, Diagnostic, DiagnosticInput, Disposable, EditOp, Envelope, ErrorCode, ErrorInfo, ExtractItem, ExtractOptions, ExtractTemplate, Extractor, FileChange, FileEvent, FileScan, FinderContext, Fix, Format, FormatContext, GlobOptions, GlossaryEntry, HardString, HistoryConfigs, HistoryContext, HistoryOptions, HistoryProvider, HistorySettings, Host, JsonObject, JsonPrimitive, JsonValue, KeyContext, KeyId, KeyInfo, KeyQuery, KeyString, KeyTarget, LineIndex, LinePosition, LocaleCode, LocaleCoverage, LocaleFinder, LocaleInfo, Location, Logger, ManifestReader, MemoryHost, Message, MessageChange, MessageHistory, MessageLeaf, MessageOp, MessageTree, NamespaceFromPath, NamespaceOptions, NodeRange, OverlayHost, ParseResult, Placeholder, PlaceholderComparer, PlaceholderProblem, PlaceholderSyntax, Plan, Plugin, PluginConfigs, PluginFile, PluginOption, PluginRegistry, PluginSpecifier, Preset, PresetExplanation, PresetFactory, PresetOption, Project, ProjectChange, ProjectExplanation, PromptContext, PromptData, PromptDraft, PromptItem, PromptOptions, RegexScannerSpec, ReportInput, ReportSummary, Reporter, ReporterContext, ResolveConfigOptions, ResolvedConfig, Resource, ResourceBlock, ResourceCandidate, ResourceId, ResourceNode, ResourceSpec, ResourceTransform, ResourceTransformContext, ScanContext, ScanResult, Scanner, SchemaIssue, Scope, SerializableConfig, Setup, Severity, SourceFile, TextEdit, TextRange, TreeLeaf, Usage, UsageCandidate, UsageContext, UsageKind, UsageMatch, UsageOptions, UsageQuery, UserConfig, WireDiagnostic, WireRange, WriteOptions, X18nError };
|
|
1476
|
+
export type { Analysis, Answers, ApplyResult, ArgSource, Builtins, CacheStore, CallSpec, CapabilityConflict, Check, CheckContext, CheckIds, CheckLevel, CheckSetting, ConfigContext, ConfigDraft, ConfigOrigin, CreateProjectOptions, Decision, DecisionOption, DetectContext, Diagnostic, DiagnosticInput, Disposable, EditOp, Envelope, ErrorCode, ErrorInfo, ExtractItem, ExtractOptions, ExtractTemplate, Extractor, FileChange, FileEvent, FileScan, FinderContext, Fix, Format, FormatContext, GlobOptions, GlossaryEntry, HardString, HistoryConfigs, HistoryContext, HistoryOptions, HistoryProvider, HistorySettings, Host, JsonObject, JsonPrimitive, JsonValue, KeyContext, KeyId, KeyInfo, KeyQuery, KeyString, KeyTarget, LineIndex, LinePosition, LocaleCode, LocaleCoverage, LocaleFinder, LocaleInfo, Location, Logger, ManifestReader, MemoryHost, Message, MessageChange, MessageHistory, MessageLeaf, MessageOp, MessageTree, NamespaceFromPath, NamespaceOptions, NodeRange, OverlayHost, ParseResult, Placeholder, PlaceholderComparer, PlaceholderProblem, PlaceholderSyntax, Plan, Plugin, PluginConfigs, PluginFile, PluginOption, PluginRegistry, PluginSpecifier, PluralForm, Preset, PresetExplanation, PresetFactory, PresetOption, Project, ProjectChange, ProjectExplanation, PromptContext, PromptData, PromptDraft, PromptItem, PromptOptions, RegexScannerSpec, ReportInput, ReportSummary, Reporter, ReporterContext, ResolveConfigOptions, ResolvedConfig, Resource, ResourceBlock, ResourceCandidate, ResourceId, ResourceNode, ResourceSpec, ResourceTransform, ResourceTransformContext, ScanContext, ScanResult, Scanner, SchemaIssue, Scope, SerializableConfig, Setup, Severity, SourceFile, TextEdit, TextRange, TreeLeaf, Usage, UsageCandidate, UsageContext, UsageKind, UsageMatch, UsageOptions, UsageQuery, UserConfig, WireDiagnostic, WireRange, WriteOptions, X18nError };
|
package/dist/index.mjs
CHANGED
|
@@ -418,8 +418,10 @@ function parseLocaleInfo(code) {
|
|
|
418
418
|
region
|
|
419
419
|
].filter(Boolean).join("-");
|
|
420
420
|
const displayName = language ? safe(() => new Intl.DisplayNames(["en"], { type: "language" }).of(tag)) : void 0;
|
|
421
|
-
const
|
|
422
|
-
|
|
421
|
+
const categories = (type) => {
|
|
422
|
+
const found = language ? safe(() => new Intl.PluralRules(tag, { type }).resolvedOptions().pluralCategories) : void 0;
|
|
423
|
+
return found ? pluralOrder.filter((category) => found.includes(category)) : ["other"];
|
|
424
|
+
};
|
|
423
425
|
const flagRegion = region ?? (language === "zh" && script === "Hant" ? "TW" : language ? defaultRegions[language] : void 0);
|
|
424
426
|
return {
|
|
425
427
|
code,
|
|
@@ -427,7 +429,8 @@ function parseLocaleInfo(code) {
|
|
|
427
429
|
region,
|
|
428
430
|
displayName,
|
|
429
431
|
flag: flagRegion ? flagOf(flagRegion) : void 0,
|
|
430
|
-
pluralCategories
|
|
432
|
+
pluralCategories: categories("cardinal"),
|
|
433
|
+
ordinalCategories: categories("ordinal")
|
|
431
434
|
};
|
|
432
435
|
}
|
|
433
436
|
/** Codes that differ only in spelling (`zh_CN` and `zh-cn`), the same after a lenient normalization. */
|
|
@@ -706,6 +709,7 @@ const extractOptions = v.strictObject({
|
|
|
706
709
|
reuseExisting: v.optional(v.boolean()),
|
|
707
710
|
ignore: v.optional(strings),
|
|
708
711
|
ignoreByFile: v.optional(v.record(v.string(), strings)),
|
|
712
|
+
attributes: v.optional(strings),
|
|
709
713
|
ignoreAttributes: v.optional(strings),
|
|
710
714
|
ignoreCallees: v.optional(strings),
|
|
711
715
|
templates: v.optional(extractTemplates)
|
|
@@ -938,6 +942,148 @@ function createHistory(input) {
|
|
|
938
942
|
};
|
|
939
943
|
}
|
|
940
944
|
//#endregion
|
|
945
|
+
//#region src/project/usages.ts
|
|
946
|
+
function innermost(scopes, range) {
|
|
947
|
+
let best;
|
|
948
|
+
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;
|
|
949
|
+
return best;
|
|
950
|
+
}
|
|
951
|
+
function createUsageResolver(config, namespaced, normalizers) {
|
|
952
|
+
const separator = config.namespace.separator;
|
|
953
|
+
return { resolve(candidate, scopes, path, scanner) {
|
|
954
|
+
const scope = innermost(scopes, candidate.range);
|
|
955
|
+
const ctx = { path };
|
|
956
|
+
let text = candidate.raw;
|
|
957
|
+
for (const normalize of normalizers) {
|
|
958
|
+
const result = normalize(text, ctx);
|
|
959
|
+
if (result !== void 0) {
|
|
960
|
+
text = result;
|
|
961
|
+
break;
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
const scopeNamespace = candidate.namespace ?? scope?.namespace;
|
|
965
|
+
const scopePrefix = candidate.keyPrefix ?? scope?.keyPrefix;
|
|
966
|
+
let namespace;
|
|
967
|
+
let explicit = false;
|
|
968
|
+
if (namespaced) {
|
|
969
|
+
const index = text.indexOf(separator);
|
|
970
|
+
explicit = index > 0;
|
|
971
|
+
if (explicit) {
|
|
972
|
+
namespace = text.slice(0, index);
|
|
973
|
+
text = text.slice(index + separator.length);
|
|
974
|
+
} else namespace = scopeNamespace ?? config.namespace.default;
|
|
975
|
+
}
|
|
976
|
+
if (scopePrefix && !explicit) text = text ? `${scopePrefix}.${text}` : scopePrefix;
|
|
977
|
+
let key;
|
|
978
|
+
if (candidate.dynamic === "unknown") key = void 0;
|
|
979
|
+
else key = joinKey(namespace, text === "" ? [] : text.split("."));
|
|
980
|
+
return {
|
|
981
|
+
key,
|
|
982
|
+
raw: candidate.raw,
|
|
983
|
+
path,
|
|
984
|
+
range: candidate.range,
|
|
985
|
+
expression: candidate.expression,
|
|
986
|
+
scanner,
|
|
987
|
+
kind: candidate.kind,
|
|
988
|
+
dynamic: candidate.dynamic,
|
|
989
|
+
locale: candidate.locale,
|
|
990
|
+
scopeNamespace,
|
|
991
|
+
scopePrefix
|
|
992
|
+
};
|
|
993
|
+
} };
|
|
994
|
+
}
|
|
995
|
+
/** True when a prefix usage may refer to `key`. The key of `t(`pages.${id}`)` is `pages.`, so only children match. */
|
|
996
|
+
function prefixMatches(usage, key) {
|
|
997
|
+
return usage.dynamic === "prefix" && usage.key !== void 0 && key.startsWith(usage.key);
|
|
998
|
+
}
|
|
999
|
+
/** Ranges covered by `x18n-disable-next-line` and `x18n-disable` ... `x18n-enable` comments. */
|
|
1000
|
+
function suppressedRanges(text) {
|
|
1001
|
+
if (!text.includes("x18n-disable")) return [];
|
|
1002
|
+
const ranges = [];
|
|
1003
|
+
for (const match of text.matchAll(/x18n-disable-next-line/g)) {
|
|
1004
|
+
const lineEnd = text.indexOf("\n", match.index);
|
|
1005
|
+
if (lineEnd === -1) continue;
|
|
1006
|
+
const nextEnd = text.indexOf("\n", lineEnd + 1);
|
|
1007
|
+
ranges.push({
|
|
1008
|
+
start: lineEnd + 1,
|
|
1009
|
+
end: nextEnd === -1 ? text.length : nextEnd
|
|
1010
|
+
});
|
|
1011
|
+
}
|
|
1012
|
+
for (const match of text.matchAll(/x18n-disable(?![-\w])/g)) {
|
|
1013
|
+
const end = text.indexOf("x18n-enable", match.index);
|
|
1014
|
+
ranges.push({
|
|
1015
|
+
start: match.index,
|
|
1016
|
+
end: end === -1 ? text.length : end
|
|
1017
|
+
});
|
|
1018
|
+
}
|
|
1019
|
+
return ranges;
|
|
1020
|
+
}
|
|
1021
|
+
/** The plural categories of a locale's language: Japanese has only `other`. */
|
|
1022
|
+
function categoriesOf(info, type) {
|
|
1023
|
+
return type === "ordinal" ? info.ordinalCategories : info.pluralCategories;
|
|
1024
|
+
}
|
|
1025
|
+
const tokens = /(\{key\}|\{plural\}|\{ordinal\}|\*)/;
|
|
1026
|
+
const categories = [
|
|
1027
|
+
"zero",
|
|
1028
|
+
"one",
|
|
1029
|
+
"two",
|
|
1030
|
+
"few",
|
|
1031
|
+
"many",
|
|
1032
|
+
"other"
|
|
1033
|
+
];
|
|
1034
|
+
/**
|
|
1035
|
+
* Compiles derived-key rules (`{key}_plural`, `{key}_*`, `{key}_{plural}`). `*` is any key segment;
|
|
1036
|
+
* `{plural}` and `{ordinal}` are CLDR plural categories, which each locale needs its own set of.
|
|
1037
|
+
*/
|
|
1038
|
+
function derivedKeys(rules) {
|
|
1039
|
+
const compiled = rules.map((rule) => {
|
|
1040
|
+
const parts = rule.split(tokens);
|
|
1041
|
+
const type = parts.includes("{ordinal}") ? "ordinal" : parts.includes("{plural}") ? "cardinal" : void 0;
|
|
1042
|
+
const source = parts.map((part) => {
|
|
1043
|
+
if (part === "{key}") return "(?<key>.+)";
|
|
1044
|
+
if (part === "{plural}" || part === "{ordinal}") return `(?<category>${categories.join("|")})`;
|
|
1045
|
+
if (part === "*") return "[^.:]+";
|
|
1046
|
+
return part.replace(/[.+?^${}()|[\]\\]/g, "\\$&");
|
|
1047
|
+
}).join("");
|
|
1048
|
+
return {
|
|
1049
|
+
type,
|
|
1050
|
+
re: new RegExp(`^${source}$`, "d")
|
|
1051
|
+
};
|
|
1052
|
+
});
|
|
1053
|
+
const baseOf = (key) => {
|
|
1054
|
+
for (const { re } of compiled) {
|
|
1055
|
+
const base = re.exec(key)?.groups?.key;
|
|
1056
|
+
if (base) return base;
|
|
1057
|
+
}
|
|
1058
|
+
};
|
|
1059
|
+
return {
|
|
1060
|
+
basesOf(key) {
|
|
1061
|
+
const bases = [];
|
|
1062
|
+
for (let base = baseOf(key); base !== void 0; base = baseOf(base)) bases.push(base);
|
|
1063
|
+
return bases;
|
|
1064
|
+
},
|
|
1065
|
+
formOf(key) {
|
|
1066
|
+
let best;
|
|
1067
|
+
for (const { type, re } of compiled) {
|
|
1068
|
+
const match = type && re.exec(key);
|
|
1069
|
+
if (!type || !match) continue;
|
|
1070
|
+
const base = match.groups?.key;
|
|
1071
|
+
const category = categories.find((c) => c === match.groups?.category);
|
|
1072
|
+
const at = match.indices?.groups?.category;
|
|
1073
|
+
if (!base || !category || !at || best && best.base.length <= base.length) continue;
|
|
1074
|
+
const [start, end] = at;
|
|
1075
|
+
best = {
|
|
1076
|
+
base,
|
|
1077
|
+
type,
|
|
1078
|
+
category,
|
|
1079
|
+
keyOf: (other) => `${key.slice(0, start)}${other}${key.slice(end)}`
|
|
1080
|
+
};
|
|
1081
|
+
}
|
|
1082
|
+
return best;
|
|
1083
|
+
}
|
|
1084
|
+
};
|
|
1085
|
+
}
|
|
1086
|
+
//#endregion
|
|
941
1087
|
//#region src/project/analysis.ts
|
|
942
1088
|
function levelOf(setting) {
|
|
943
1089
|
if (setting === void 0) return void 0;
|
|
@@ -950,9 +1096,24 @@ function levelOf(setting) {
|
|
|
950
1096
|
options: setting[1]
|
|
951
1097
|
};
|
|
952
1098
|
}
|
|
953
|
-
|
|
1099
|
+
/**
|
|
1100
|
+
* The keys a locale needs: the reference keys, with plural forms swapped for the categories of the locale's
|
|
1101
|
+
* language. Forms the language lacks (`item_zero` in English) count only where they exist.
|
|
1102
|
+
*/
|
|
1103
|
+
function expectedKeys(state, options, reference, locale) {
|
|
1104
|
+
const info = options.localeInfo(locale);
|
|
1105
|
+
const keys = /* @__PURE__ */ new Set();
|
|
1106
|
+
for (const key of reference) {
|
|
1107
|
+
const form = options.formOf(key);
|
|
1108
|
+
if (!form || state.messages.get(key)?.has(locale)) keys.add(key);
|
|
1109
|
+
form && categoriesOf(info, form.type).forEach((category) => keys.add(form.keyOf(category)));
|
|
1110
|
+
}
|
|
1111
|
+
return [...keys].sort();
|
|
1112
|
+
}
|
|
1113
|
+
function computeCoverage(state, options, sourceKeys) {
|
|
954
1114
|
const { messages, sourceLocale } = state;
|
|
955
1115
|
return state.locales.map((locale) => {
|
|
1116
|
+
const reference = expectedKeys(state, options, sourceKeys, locale);
|
|
956
1117
|
const chain = fallbackChain(state.fallbacks, locale);
|
|
957
1118
|
const inherited = (key) => chain.some((code) => {
|
|
958
1119
|
const value = messages.get(key)?.get(code)?.[0]?.value;
|
|
@@ -1007,7 +1168,7 @@ function* runCheck(check, level, inputs, input) {
|
|
|
1007
1168
|
const severity = config.strict && (item.severity ?? level) === "warning" ? "error" : item.severity ?? level;
|
|
1008
1169
|
let location = item.location;
|
|
1009
1170
|
if (!location && item.key !== void 0) {
|
|
1010
|
-
const byLocale = state.messages.get(item.key);
|
|
1171
|
+
const byLocale = state.messages.get(item.key) ?? state.messages.get(input.ctx.formOf(item.key)?.keyOf("other") ?? item.key);
|
|
1011
1172
|
const found = byLocale?.get(item.locale ?? state.sourceLocale)?.[0] ?? byLocale?.get(state.sourceLocale)?.[0];
|
|
1012
1173
|
if (found) location = {
|
|
1013
1174
|
path: resourcePath(found.resource),
|
|
@@ -1067,7 +1228,7 @@ function createAnalyzer(options) {
|
|
|
1067
1228
|
const { sourceLocale, locales, messages } = state;
|
|
1068
1229
|
const sourceKeys = [...messages.keys()].filter((key) => messages.get(key)?.has(sourceLocale)).sort();
|
|
1069
1230
|
const allKeys = [...messages.keys()].sort();
|
|
1070
|
-
const coverage = computeCoverage(state, sourceKeys.length > 0 ? sourceKeys : allKeys);
|
|
1231
|
+
const coverage = computeCoverage(state, options, sourceKeys.length > 0 ? sourceKeys : allKeys);
|
|
1071
1232
|
const isDefined = (key) => messages.get(key)?.has(sourceLocale) === true || state.parents.has(key) || (state.derived.get(key)?.length ?? 0) > 0 || sourceKeys.length === 0 && messages.has(key);
|
|
1072
1233
|
const hasScanners = state.files.size > 0;
|
|
1073
1234
|
const referenceSyntaxes = registry.placeholders.filter((syntax) => syntax.reference);
|
|
@@ -1079,8 +1240,7 @@ function createAnalyzer(options) {
|
|
|
1079
1240
|
const prefixes = [...new Set(state.prefixUsages.flatMap((usage) => usage.key === void 0 ? [] : [usage.key]))];
|
|
1080
1241
|
const isUsed = (key) => {
|
|
1081
1242
|
if (state.usagesByKey.has(key) || inUse(key) || referenced.has(key)) return true;
|
|
1082
|
-
|
|
1083
|
-
if (base !== void 0 && (state.usagesByKey.has(base) || inUse(base))) return true;
|
|
1243
|
+
if (options.basesOf(key).some((base) => state.usagesByKey.has(base) || inUse(base))) return true;
|
|
1084
1244
|
return prefixes.some((prefix) => key.startsWith(prefix));
|
|
1085
1245
|
};
|
|
1086
1246
|
const unused = hasScanners ? allKeys.filter((key) => !isUsed(key)) : [];
|
|
@@ -1107,6 +1267,7 @@ function createAnalyzer(options) {
|
|
|
1107
1267
|
},
|
|
1108
1268
|
message: (key, locale) => messages.get(key)?.get(locale)?.[0],
|
|
1109
1269
|
duplicates: (key, locale) => messages.get(key)?.get(locale) ?? [],
|
|
1270
|
+
formOf: options.formOf,
|
|
1110
1271
|
usages: () => state.usages,
|
|
1111
1272
|
coverage: () => coverage,
|
|
1112
1273
|
unused: () => unused,
|
|
@@ -1527,7 +1688,7 @@ var PlanBuilder = class {
|
|
|
1527
1688
|
const all = this.options.allKeys();
|
|
1528
1689
|
const isLeaf = this.options.messages(op.from) !== void 0;
|
|
1529
1690
|
const under = all.filter((key) => isKeyUnder(key, op.from));
|
|
1530
|
-
const derived = all.filter((key) => this.options.
|
|
1691
|
+
const derived = all.filter((key) => this.options.basesOf(key).includes(op.from));
|
|
1531
1692
|
let keys;
|
|
1532
1693
|
if (op.type === "rename-prefix") keys = under;
|
|
1533
1694
|
else if (isLeaf) keys = [op.from, ...derived];
|
|
@@ -1919,95 +2080,6 @@ function renderPrompt(data) {
|
|
|
1919
2080
|
return out.join("\n");
|
|
1920
2081
|
}
|
|
1921
2082
|
//#endregion
|
|
1922
|
-
//#region src/project/usages.ts
|
|
1923
|
-
function innermost(scopes, range) {
|
|
1924
|
-
let best;
|
|
1925
|
-
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;
|
|
1926
|
-
return best;
|
|
1927
|
-
}
|
|
1928
|
-
function createUsageResolver(config, namespaced, normalizers) {
|
|
1929
|
-
const separator = config.namespace.separator;
|
|
1930
|
-
return { resolve(candidate, scopes, path, scanner) {
|
|
1931
|
-
const scope = innermost(scopes, candidate.range);
|
|
1932
|
-
const ctx = { path };
|
|
1933
|
-
let text = candidate.raw;
|
|
1934
|
-
for (const normalize of normalizers) {
|
|
1935
|
-
const result = normalize(text, ctx);
|
|
1936
|
-
if (result !== void 0) {
|
|
1937
|
-
text = result;
|
|
1938
|
-
break;
|
|
1939
|
-
}
|
|
1940
|
-
}
|
|
1941
|
-
const scopeNamespace = candidate.namespace ?? scope?.namespace;
|
|
1942
|
-
const scopePrefix = candidate.keyPrefix ?? scope?.keyPrefix;
|
|
1943
|
-
let namespace;
|
|
1944
|
-
let explicit = false;
|
|
1945
|
-
if (namespaced) {
|
|
1946
|
-
const index = text.indexOf(separator);
|
|
1947
|
-
explicit = index > 0;
|
|
1948
|
-
if (explicit) {
|
|
1949
|
-
namespace = text.slice(0, index);
|
|
1950
|
-
text = text.slice(index + separator.length);
|
|
1951
|
-
} else namespace = scopeNamespace ?? config.namespace.default;
|
|
1952
|
-
}
|
|
1953
|
-
if (scopePrefix && !explicit) text = text ? `${scopePrefix}.${text}` : scopePrefix;
|
|
1954
|
-
let key;
|
|
1955
|
-
if (candidate.dynamic === "unknown") key = void 0;
|
|
1956
|
-
else key = joinKey(namespace, text === "" ? [] : text.split("."));
|
|
1957
|
-
return {
|
|
1958
|
-
key,
|
|
1959
|
-
raw: candidate.raw,
|
|
1960
|
-
path,
|
|
1961
|
-
range: candidate.range,
|
|
1962
|
-
scanner,
|
|
1963
|
-
kind: candidate.kind,
|
|
1964
|
-
dynamic: candidate.dynamic,
|
|
1965
|
-
locale: candidate.locale,
|
|
1966
|
-
scopeNamespace,
|
|
1967
|
-
scopePrefix
|
|
1968
|
-
};
|
|
1969
|
-
} };
|
|
1970
|
-
}
|
|
1971
|
-
/** True when a prefix usage may refer to `key`. The key of `t(`pages.${id}`)` is `pages.`, so only children match. */
|
|
1972
|
-
function prefixMatches(usage, key) {
|
|
1973
|
-
return usage.dynamic === "prefix" && usage.key !== void 0 && key.startsWith(usage.key);
|
|
1974
|
-
}
|
|
1975
|
-
/** Ranges covered by `x18n-disable-next-line` and `x18n-disable` ... `x18n-enable` comments. */
|
|
1976
|
-
function suppressedRanges(text) {
|
|
1977
|
-
if (!text.includes("x18n-disable")) return [];
|
|
1978
|
-
const ranges = [];
|
|
1979
|
-
for (const match of text.matchAll(/x18n-disable-next-line/g)) {
|
|
1980
|
-
const lineEnd = text.indexOf("\n", match.index);
|
|
1981
|
-
if (lineEnd === -1) continue;
|
|
1982
|
-
const nextEnd = text.indexOf("\n", lineEnd + 1);
|
|
1983
|
-
ranges.push({
|
|
1984
|
-
start: lineEnd + 1,
|
|
1985
|
-
end: nextEnd === -1 ? text.length : nextEnd
|
|
1986
|
-
});
|
|
1987
|
-
}
|
|
1988
|
-
for (const match of text.matchAll(/x18n-disable(?![-\w])/g)) {
|
|
1989
|
-
const end = text.indexOf("x18n-enable", match.index);
|
|
1990
|
-
ranges.push({
|
|
1991
|
-
start: match.index,
|
|
1992
|
-
end: end === -1 ? text.length : end
|
|
1993
|
-
});
|
|
1994
|
-
}
|
|
1995
|
-
return ranges;
|
|
1996
|
-
}
|
|
1997
|
-
/** Compiles derived-key rules (`{key}_plural`, `{key}_*`) into a function from a key to its base key. */
|
|
1998
|
-
function derivedBase(rules) {
|
|
1999
|
-
const regexes = rules.map((rule) => {
|
|
2000
|
-
const source = rule.split("{key}").map((part) => part.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, "[^.:]+")).join("(.+)");
|
|
2001
|
-
return new RegExp(`^${source}$`);
|
|
2002
|
-
});
|
|
2003
|
-
return (key) => {
|
|
2004
|
-
for (const re of regexes) {
|
|
2005
|
-
const match = re.exec(key);
|
|
2006
|
-
if (match?.[1]) return match[1];
|
|
2007
|
-
}
|
|
2008
|
-
};
|
|
2009
|
-
}
|
|
2010
|
-
//#endregion
|
|
2011
2083
|
//#region src/project/project.ts
|
|
2012
2084
|
const scanConcurrency = 64;
|
|
2013
2085
|
function createProject(options) {
|
|
@@ -2033,7 +2105,7 @@ function createProject(options) {
|
|
|
2033
2105
|
const normalizers = registry.plugins.flatMap((plugin) => plugin.normalizeKey ? [plugin.normalizeKey] : []);
|
|
2034
2106
|
const transforms = registry.plugins.flatMap((plugin) => plugin.transformResource ? [plugin.transformResource] : []);
|
|
2035
2107
|
const localeInfoHooks = registry.plugins.flatMap((plugin) => plugin.localeInfo ? [plugin.localeInfo] : []);
|
|
2036
|
-
const
|
|
2108
|
+
const { basesOf, formOf } = derivedKeys(config.usage.derivedKeys);
|
|
2037
2109
|
const scanSignature = hashText(JSON.stringify({
|
|
2038
2110
|
usage: config.usage,
|
|
2039
2111
|
namespace: config.namespace,
|
|
@@ -2061,6 +2133,11 @@ function createProject(options) {
|
|
|
2061
2133
|
}
|
|
2062
2134
|
return info;
|
|
2063
2135
|
}
|
|
2136
|
+
/** Whether `locale` needs `key`: not a plural form of a category its language lacks. */
|
|
2137
|
+
function needs(key, locale) {
|
|
2138
|
+
const form = formOf(key);
|
|
2139
|
+
return !form || categoriesOf(localeInfo(locale), form.type).includes(form.category);
|
|
2140
|
+
}
|
|
2064
2141
|
async function discover() {
|
|
2065
2142
|
const ctx = {
|
|
2066
2143
|
host,
|
|
@@ -2379,13 +2456,10 @@ function createProject(options) {
|
|
|
2379
2456
|
}
|
|
2380
2457
|
for (const { namespace, path } of keyPaths.values()) for (let i = 1; i < path.length; i++) parents.add(joinKey(namespace, path.slice(0, i)));
|
|
2381
2458
|
const derived = /* @__PURE__ */ new Map();
|
|
2382
|
-
if (config.usage.derivedKeys.length > 0) for (const key of messages.keys()) {
|
|
2383
|
-
const
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
derived.set(base, list);
|
|
2387
|
-
list.push(key);
|
|
2388
|
-
}
|
|
2459
|
+
if (config.usage.derivedKeys.length > 0) for (const key of messages.keys()) for (const base of basesOf(key)) {
|
|
2460
|
+
const list = derived.get(base) ?? [];
|
|
2461
|
+
derived.set(base, list);
|
|
2462
|
+
list.push(key);
|
|
2389
2463
|
}
|
|
2390
2464
|
return {
|
|
2391
2465
|
messages,
|
|
@@ -2457,8 +2531,7 @@ function createProject(options) {
|
|
|
2457
2531
|
const inPath = (usage) => query.path === void 0 || usage.path === query.path;
|
|
2458
2532
|
if (query.key !== void 0) {
|
|
2459
2533
|
const key = query.key;
|
|
2460
|
-
|
|
2461
|
-
return [...[...usagesByKey.get(key) ?? [], ...base ? usagesByKey.get(base) ?? [] : []].filter(inPath).map((usage) => ({
|
|
2534
|
+
return [...[key, ...basesOf(key)].flatMap((k) => usagesByKey.get(k) ?? []).filter(inPath).map((usage) => ({
|
|
2462
2535
|
usage,
|
|
2463
2536
|
possible: false
|
|
2464
2537
|
})), ...prefixUsages.filter((usage) => inPath(usage) && prefixMatches(usage, key)).map((usage) => ({
|
|
@@ -2508,7 +2581,8 @@ function createProject(options) {
|
|
|
2508
2581
|
files,
|
|
2509
2582
|
history: history.get
|
|
2510
2583
|
}),
|
|
2511
|
-
|
|
2584
|
+
basesOf,
|
|
2585
|
+
formOf,
|
|
2512
2586
|
localeInfo,
|
|
2513
2587
|
text: (path) => fileTexts.get(path) ?? files.get(path)?.text
|
|
2514
2588
|
});
|
|
@@ -2522,7 +2596,7 @@ function createProject(options) {
|
|
|
2522
2596
|
const locale = query.missing;
|
|
2523
2597
|
list = list.filter((key) => {
|
|
2524
2598
|
const value = message(key, locale)?.value;
|
|
2525
|
-
return message(key, sourceLocale) !== void 0 && (value === void 0 || value === "");
|
|
2599
|
+
return message(key, sourceLocale) !== void 0 && (value === void 0 || value === "") && needs(key, locale);
|
|
2526
2600
|
});
|
|
2527
2601
|
}
|
|
2528
2602
|
if (query.unused) {
|
|
@@ -2542,7 +2616,7 @@ function createProject(options) {
|
|
|
2542
2616
|
namespace: id.namespace,
|
|
2543
2617
|
path: id.path,
|
|
2544
2618
|
messages: list,
|
|
2545
|
-
missing: locales.filter((locale) => !byLocale.has(locale)),
|
|
2619
|
+
missing: locales.filter((locale) => !byLocale.has(locale) && needs(key, locale)),
|
|
2546
2620
|
usages: usageMatches({ key }),
|
|
2547
2621
|
diagnostics: analyzer.analyze().diagnostics.filter((d) => d.key === key)
|
|
2548
2622
|
};
|
|
@@ -2674,7 +2748,7 @@ function createProject(options) {
|
|
|
2674
2748
|
parents: () => idx().parents,
|
|
2675
2749
|
usageMatches,
|
|
2676
2750
|
text: (path) => fileTexts.get(path) ?? files.get(path)?.text,
|
|
2677
|
-
|
|
2751
|
+
basesOf,
|
|
2678
2752
|
toStored: (target, path) => storedPathOf(target)(path),
|
|
2679
2753
|
resolveNew(locale, namespace) {
|
|
2680
2754
|
const ctx = {
|
|
@@ -2748,7 +2822,7 @@ function createProject(options) {
|
|
|
2748
2822
|
lookup(key, locale) {
|
|
2749
2823
|
const exact = message(key, locale);
|
|
2750
2824
|
if (exact) return exact;
|
|
2751
|
-
const derived =
|
|
2825
|
+
const derived = (idx().derived.get(key) ?? []).filter((k) => message(k, locale) !== void 0).sort((a, b) => a.length - b.length || a.localeCompare(b));
|
|
2752
2826
|
const other = derived.find((k) => /[._-]other$/.test(k)) ?? derived[0];
|
|
2753
2827
|
return other === void 0 ? void 0 : message(other, locale);
|
|
2754
2828
|
},
|
package/package.json
CHANGED