@x18n/core 0.0.10 → 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 +30 -4
- package/dist/index.mjs +236 -117
- package/package.json +1 -1
- package/schema/config.json +22 -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,10 +577,19 @@ 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;
|
|
577
590
|
readonly locales: readonly LocaleCode[];
|
|
591
|
+
/** The locales a locale falls back to, nearest first. Empty for a locale without a fallback. */
|
|
592
|
+
readonly fallbacks: (locale: LocaleCode) => readonly LocaleCode[];
|
|
578
593
|
readonly resources: readonly Resource[];
|
|
579
594
|
/** Options from `[severity, options]` in config. */
|
|
580
595
|
readonly options: JsonObject;
|
|
@@ -585,6 +600,8 @@ interface CheckContext {
|
|
|
585
600
|
readonly message: (key: KeyString, locale: LocaleCode) => Message | undefined;
|
|
586
601
|
/** Every resource message of a key in a locale, more than one when duplicated across files. */
|
|
587
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;
|
|
588
605
|
readonly usages: () => Iterable<Usage>;
|
|
589
606
|
readonly coverage: () => readonly LocaleCoverage[];
|
|
590
607
|
readonly unused: () => readonly KeyString[];
|
|
@@ -783,12 +800,14 @@ interface ExtractOptions {
|
|
|
783
800
|
readonly keyMaxLength?: number | undefined;
|
|
784
801
|
/** Reuse a key when the same text exists as a source value. Default true. */
|
|
785
802
|
readonly reuseExisting?: boolean | undefined;
|
|
786
|
-
/** 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. */
|
|
787
804
|
readonly ignore?: readonly string[] | undefined;
|
|
788
|
-
/**
|
|
805
|
+
/** Strings never extracted in the files of a glob, written as in `ignore`. */
|
|
789
806
|
readonly ignoreByFile?: {
|
|
790
807
|
readonly [glob: string]: readonly string[];
|
|
791
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;
|
|
792
811
|
readonly ignoreAttributes?: readonly string[] | undefined;
|
|
793
812
|
readonly ignoreCallees?: readonly string[] | undefined;
|
|
794
813
|
readonly templates?: readonly (string | ExtractTemplate)[] | undefined;
|
|
@@ -802,7 +821,7 @@ interface UsageOptions {
|
|
|
802
821
|
readonly ignoreCallees?: readonly string[] | undefined;
|
|
803
822
|
/** Keys (globs) that count as used, for example keys built at runtime. */
|
|
804
823
|
readonly keysInUse?: readonly string[] | undefined;
|
|
805
|
-
/** 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. */
|
|
806
825
|
readonly derivedKeys?: readonly string[] | undefined;
|
|
807
826
|
}
|
|
808
827
|
interface WriteOptions {
|
|
@@ -832,6 +851,10 @@ interface SerializableConfig {
|
|
|
832
851
|
/** Locale codes as written in paths. Filters the found locales. */
|
|
833
852
|
readonly locales?: readonly string[] | undefined;
|
|
834
853
|
readonly ignoreLocales?: readonly string[] | undefined;
|
|
854
|
+
/** Locale to the locale it falls back to, such as `{ 'en-GB': 'en' }`. A locale with a fallback holds only overrides: a key it lacks or leaves empty is not missing when the fallback chain has it. `'auto'` falls back to the nearest parent language that exists: `en-US` to `en`, `zh-Hant-TW` to `zh-Hant` or `zh`. */
|
|
855
|
+
readonly localeFallbacks?: "auto" | {
|
|
856
|
+
readonly [locale: string]: string;
|
|
857
|
+
} | undefined;
|
|
835
858
|
/** `'auto'` detects built-in presets from the project's dependencies. */
|
|
836
859
|
readonly presets?: "auto" | readonly PluginSpecifier[] | undefined;
|
|
837
860
|
readonly plugins?: readonly PluginSpecifier[] | undefined;
|
|
@@ -892,6 +915,9 @@ interface ResolvedConfig {
|
|
|
892
915
|
readonly sourceLocale: string;
|
|
893
916
|
readonly locales: readonly string[] | undefined;
|
|
894
917
|
readonly ignoreLocales: readonly string[];
|
|
918
|
+
readonly localeFallbacks: "auto" | {
|
|
919
|
+
readonly [locale: string]: string;
|
|
920
|
+
};
|
|
895
921
|
readonly resources: readonly ResourceSpec[];
|
|
896
922
|
readonly formats: {
|
|
897
923
|
readonly [extension: string]: string;
|
|
@@ -1447,4 +1473,4 @@ export declare function rangeContains(range: TextRange, offset: number): boolean
|
|
|
1447
1473
|
/** Fast non-cryptographic string hash (cyrb53), as hex. */
|
|
1448
1474
|
export declare function hashText(text: string): string;
|
|
1449
1475
|
//#endregion
|
|
1450
|
-
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
|
@@ -363,6 +363,28 @@ const defaultRegions = {
|
|
|
363
363
|
vi: "VN",
|
|
364
364
|
zh: "CN"
|
|
365
365
|
};
|
|
366
|
+
/** The fallback of each locale: from the config map, or for `'auto'` the nearest parent language among `locales`. The source locale has none. */
|
|
367
|
+
function resolveFallbacks(setting, locales, sourceLocale) {
|
|
368
|
+
if (setting !== "auto") return Object.fromEntries(Object.entries(setting).filter(([locale]) => locale !== sourceLocale));
|
|
369
|
+
const fallbacks = {};
|
|
370
|
+
for (const locale of locales) {
|
|
371
|
+
if (locale === sourceLocale) continue;
|
|
372
|
+
for (let cut = locale.search(/[-_][^-_]*$/); cut > 0; cut = locale.slice(0, cut).search(/[-_][^-_]*$/)) {
|
|
373
|
+
const parent = locales.find((code) => code === locale.slice(0, cut));
|
|
374
|
+
if (parent) {
|
|
375
|
+
fallbacks[locale] = parent;
|
|
376
|
+
break;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
return fallbacks;
|
|
381
|
+
}
|
|
382
|
+
/** The locales a locale falls back to, nearest first. Stops at a cycle. */
|
|
383
|
+
function fallbackChain(fallbacks, locale) {
|
|
384
|
+
const chain = [];
|
|
385
|
+
for (let next = fallbacks[locale]; next !== void 0 && next !== locale && !chain.includes(next); next = fallbacks[next]) chain.push(next);
|
|
386
|
+
return chain;
|
|
387
|
+
}
|
|
366
388
|
const pluralOrder = [
|
|
367
389
|
"zero",
|
|
368
390
|
"one",
|
|
@@ -396,8 +418,10 @@ function parseLocaleInfo(code) {
|
|
|
396
418
|
region
|
|
397
419
|
].filter(Boolean).join("-");
|
|
398
420
|
const displayName = language ? safe(() => new Intl.DisplayNames(["en"], { type: "language" }).of(tag)) : void 0;
|
|
399
|
-
const
|
|
400
|
-
|
|
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
|
+
};
|
|
401
425
|
const flagRegion = region ?? (language === "zh" && script === "Hant" ? "TW" : language ? defaultRegions[language] : void 0);
|
|
402
426
|
return {
|
|
403
427
|
code,
|
|
@@ -405,7 +429,8 @@ function parseLocaleInfo(code) {
|
|
|
405
429
|
region,
|
|
406
430
|
displayName,
|
|
407
431
|
flag: flagRegion ? flagOf(flagRegion) : void 0,
|
|
408
|
-
pluralCategories
|
|
432
|
+
pluralCategories: categories("cardinal"),
|
|
433
|
+
ordinalCategories: categories("ordinal")
|
|
409
434
|
};
|
|
410
435
|
}
|
|
411
436
|
/** Codes that differ only in spelling (`zh_CN` and `zh-cn`), the same after a lenient normalization. */
|
|
@@ -684,6 +709,7 @@ const extractOptions = v.strictObject({
|
|
|
684
709
|
reuseExisting: v.optional(v.boolean()),
|
|
685
710
|
ignore: v.optional(strings),
|
|
686
711
|
ignoreByFile: v.optional(v.record(v.string(), strings)),
|
|
712
|
+
attributes: v.optional(strings),
|
|
687
713
|
ignoreAttributes: v.optional(strings),
|
|
688
714
|
ignoreCallees: v.optional(strings),
|
|
689
715
|
templates: v.optional(extractTemplates)
|
|
@@ -712,6 +738,7 @@ const ConfigSchema = v.strictObject({
|
|
|
712
738
|
sourceLocale: v.optional(v.string()),
|
|
713
739
|
locales: v.optional(strings),
|
|
714
740
|
ignoreLocales: v.optional(strings),
|
|
741
|
+
localeFallbacks: v.optional(v.union([v.literal("auto"), v.record(v.string(), v.string())])),
|
|
715
742
|
presets: v.optional(v.union([v.literal("auto"), v.array(pluginSpecifier)])),
|
|
716
743
|
plugins: v.optional(v.array(pluginSpecifier)),
|
|
717
744
|
resources: v.optional(v.array(resourceSpec)),
|
|
@@ -915,6 +942,148 @@ function createHistory(input) {
|
|
|
915
942
|
};
|
|
916
943
|
}
|
|
917
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
|
|
918
1087
|
//#region src/project/analysis.ts
|
|
919
1088
|
function levelOf(setting) {
|
|
920
1089
|
if (setting === void 0) return void 0;
|
|
@@ -927,17 +1096,38 @@ function levelOf(setting) {
|
|
|
927
1096
|
options: setting[1]
|
|
928
1097
|
};
|
|
929
1098
|
}
|
|
930
|
-
|
|
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) {
|
|
931
1114
|
const { messages, sourceLocale } = state;
|
|
932
1115
|
return state.locales.map((locale) => {
|
|
1116
|
+
const reference = expectedKeys(state, options, sourceKeys, locale);
|
|
1117
|
+
const chain = fallbackChain(state.fallbacks, locale);
|
|
1118
|
+
const inherited = (key) => chain.some((code) => {
|
|
1119
|
+
const value = messages.get(key)?.get(code)?.[0]?.value;
|
|
1120
|
+
return value !== void 0 && value !== "";
|
|
1121
|
+
});
|
|
933
1122
|
const missing = [];
|
|
934
1123
|
const empty = [];
|
|
935
1124
|
const outdated = [];
|
|
936
1125
|
for (const key of reference) {
|
|
937
1126
|
const value = messages.get(key)?.get(locale)?.[0]?.value;
|
|
938
|
-
if (value === void 0)
|
|
939
|
-
|
|
940
|
-
|
|
1127
|
+
if (value === void 0 || value === "") {
|
|
1128
|
+
if (inherited(key)) continue;
|
|
1129
|
+
(value === void 0 ? missing : empty).push(key);
|
|
1130
|
+
} else if (locale !== sourceLocale && isOutdated(state.history(key, sourceLocale), state.history(key, locale))) outdated.push(key);
|
|
941
1131
|
}
|
|
942
1132
|
return {
|
|
943
1133
|
locale,
|
|
@@ -978,7 +1168,7 @@ function* runCheck(check, level, inputs, input) {
|
|
|
978
1168
|
const severity = config.strict && (item.severity ?? level) === "warning" ? "error" : item.severity ?? level;
|
|
979
1169
|
let location = item.location;
|
|
980
1170
|
if (!location && item.key !== void 0) {
|
|
981
|
-
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);
|
|
982
1172
|
const found = byLocale?.get(item.locale ?? state.sourceLocale)?.[0] ?? byLocale?.get(state.sourceLocale)?.[0];
|
|
983
1173
|
if (found) location = {
|
|
984
1174
|
path: resourcePath(found.resource),
|
|
@@ -1038,7 +1228,7 @@ function createAnalyzer(options) {
|
|
|
1038
1228
|
const { sourceLocale, locales, messages } = state;
|
|
1039
1229
|
const sourceKeys = [...messages.keys()].filter((key) => messages.get(key)?.has(sourceLocale)).sort();
|
|
1040
1230
|
const allKeys = [...messages.keys()].sort();
|
|
1041
|
-
const coverage = computeCoverage(state, sourceKeys.length > 0 ? sourceKeys : allKeys);
|
|
1231
|
+
const coverage = computeCoverage(state, options, sourceKeys.length > 0 ? sourceKeys : allKeys);
|
|
1042
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);
|
|
1043
1233
|
const hasScanners = state.files.size > 0;
|
|
1044
1234
|
const referenceSyntaxes = registry.placeholders.filter((syntax) => syntax.reference);
|
|
@@ -1050,8 +1240,7 @@ function createAnalyzer(options) {
|
|
|
1050
1240
|
const prefixes = [...new Set(state.prefixUsages.flatMap((usage) => usage.key === void 0 ? [] : [usage.key]))];
|
|
1051
1241
|
const isUsed = (key) => {
|
|
1052
1242
|
if (state.usagesByKey.has(key) || inUse(key) || referenced.has(key)) return true;
|
|
1053
|
-
|
|
1054
|
-
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;
|
|
1055
1244
|
return prefixes.some((prefix) => key.startsWith(prefix));
|
|
1056
1245
|
};
|
|
1057
1246
|
const unused = hasScanners ? allKeys.filter((key) => !isUsed(key)) : [];
|
|
@@ -1069,6 +1258,7 @@ function createAnalyzer(options) {
|
|
|
1069
1258
|
config,
|
|
1070
1259
|
sourceLocale,
|
|
1071
1260
|
locales,
|
|
1261
|
+
fallbacks: (locale) => fallbackChain(state.fallbacks, locale),
|
|
1072
1262
|
resources: state.resources.map((s) => s.resource),
|
|
1073
1263
|
hasScanners,
|
|
1074
1264
|
keys: () => allKeys,
|
|
@@ -1077,6 +1267,7 @@ function createAnalyzer(options) {
|
|
|
1077
1267
|
},
|
|
1078
1268
|
message: (key, locale) => messages.get(key)?.get(locale)?.[0],
|
|
1079
1269
|
duplicates: (key, locale) => messages.get(key)?.get(locale) ?? [],
|
|
1270
|
+
formOf: options.formOf,
|
|
1080
1271
|
usages: () => state.usages,
|
|
1081
1272
|
coverage: () => coverage,
|
|
1082
1273
|
unused: () => unused,
|
|
@@ -1497,7 +1688,7 @@ var PlanBuilder = class {
|
|
|
1497
1688
|
const all = this.options.allKeys();
|
|
1498
1689
|
const isLeaf = this.options.messages(op.from) !== void 0;
|
|
1499
1690
|
const under = all.filter((key) => isKeyUnder(key, op.from));
|
|
1500
|
-
const derived = all.filter((key) => this.options.
|
|
1691
|
+
const derived = all.filter((key) => this.options.basesOf(key).includes(op.from));
|
|
1501
1692
|
let keys;
|
|
1502
1693
|
if (op.type === "rename-prefix") keys = under;
|
|
1503
1694
|
else if (isLeaf) keys = [op.from, ...derived];
|
|
@@ -1543,7 +1734,7 @@ var PlanBuilder = class {
|
|
|
1543
1734
|
case "set":
|
|
1544
1735
|
this.setValue(op.key, op.locale, op.value);
|
|
1545
1736
|
if (this.config.write.keepFilled) {
|
|
1546
|
-
for (const locale of this.state.locales) if (locale !== op.locale && !this.options.message(op.key, locale)) this.setValue(op.key, locale, "");
|
|
1737
|
+
for (const locale of this.state.locales) if (locale !== op.locale && !this.state.fallbacks[locale] && !this.options.message(op.key, locale)) this.setValue(op.key, locale, "");
|
|
1547
1738
|
}
|
|
1548
1739
|
break;
|
|
1549
1740
|
case "delete":
|
|
@@ -1889,95 +2080,6 @@ function renderPrompt(data) {
|
|
|
1889
2080
|
return out.join("\n");
|
|
1890
2081
|
}
|
|
1891
2082
|
//#endregion
|
|
1892
|
-
//#region src/project/usages.ts
|
|
1893
|
-
function innermost(scopes, range) {
|
|
1894
|
-
let best;
|
|
1895
|
-
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;
|
|
1896
|
-
return best;
|
|
1897
|
-
}
|
|
1898
|
-
function createUsageResolver(config, namespaced, normalizers) {
|
|
1899
|
-
const separator = config.namespace.separator;
|
|
1900
|
-
return { resolve(candidate, scopes, path, scanner) {
|
|
1901
|
-
const scope = innermost(scopes, candidate.range);
|
|
1902
|
-
const ctx = { path };
|
|
1903
|
-
let text = candidate.raw;
|
|
1904
|
-
for (const normalize of normalizers) {
|
|
1905
|
-
const result = normalize(text, ctx);
|
|
1906
|
-
if (result !== void 0) {
|
|
1907
|
-
text = result;
|
|
1908
|
-
break;
|
|
1909
|
-
}
|
|
1910
|
-
}
|
|
1911
|
-
const scopeNamespace = candidate.namespace ?? scope?.namespace;
|
|
1912
|
-
const scopePrefix = candidate.keyPrefix ?? scope?.keyPrefix;
|
|
1913
|
-
let namespace;
|
|
1914
|
-
let explicit = false;
|
|
1915
|
-
if (namespaced) {
|
|
1916
|
-
const index = text.indexOf(separator);
|
|
1917
|
-
explicit = index > 0;
|
|
1918
|
-
if (explicit) {
|
|
1919
|
-
namespace = text.slice(0, index);
|
|
1920
|
-
text = text.slice(index + separator.length);
|
|
1921
|
-
} else namespace = scopeNamespace ?? config.namespace.default;
|
|
1922
|
-
}
|
|
1923
|
-
if (scopePrefix && !explicit) text = text ? `${scopePrefix}.${text}` : scopePrefix;
|
|
1924
|
-
let key;
|
|
1925
|
-
if (candidate.dynamic === "unknown") key = void 0;
|
|
1926
|
-
else key = joinKey(namespace, text === "" ? [] : text.split("."));
|
|
1927
|
-
return {
|
|
1928
|
-
key,
|
|
1929
|
-
raw: candidate.raw,
|
|
1930
|
-
path,
|
|
1931
|
-
range: candidate.range,
|
|
1932
|
-
scanner,
|
|
1933
|
-
kind: candidate.kind,
|
|
1934
|
-
dynamic: candidate.dynamic,
|
|
1935
|
-
locale: candidate.locale,
|
|
1936
|
-
scopeNamespace,
|
|
1937
|
-
scopePrefix
|
|
1938
|
-
};
|
|
1939
|
-
} };
|
|
1940
|
-
}
|
|
1941
|
-
/** True when a prefix usage may refer to `key`. The key of `t(`pages.${id}`)` is `pages.`, so only children match. */
|
|
1942
|
-
function prefixMatches(usage, key) {
|
|
1943
|
-
return usage.dynamic === "prefix" && usage.key !== void 0 && key.startsWith(usage.key);
|
|
1944
|
-
}
|
|
1945
|
-
/** Ranges covered by `x18n-disable-next-line` and `x18n-disable` ... `x18n-enable` comments. */
|
|
1946
|
-
function suppressedRanges(text) {
|
|
1947
|
-
if (!text.includes("x18n-disable")) return [];
|
|
1948
|
-
const ranges = [];
|
|
1949
|
-
for (const match of text.matchAll(/x18n-disable-next-line/g)) {
|
|
1950
|
-
const lineEnd = text.indexOf("\n", match.index);
|
|
1951
|
-
if (lineEnd === -1) continue;
|
|
1952
|
-
const nextEnd = text.indexOf("\n", lineEnd + 1);
|
|
1953
|
-
ranges.push({
|
|
1954
|
-
start: lineEnd + 1,
|
|
1955
|
-
end: nextEnd === -1 ? text.length : nextEnd
|
|
1956
|
-
});
|
|
1957
|
-
}
|
|
1958
|
-
for (const match of text.matchAll(/x18n-disable(?![-\w])/g)) {
|
|
1959
|
-
const end = text.indexOf("x18n-enable", match.index);
|
|
1960
|
-
ranges.push({
|
|
1961
|
-
start: match.index,
|
|
1962
|
-
end: end === -1 ? text.length : end
|
|
1963
|
-
});
|
|
1964
|
-
}
|
|
1965
|
-
return ranges;
|
|
1966
|
-
}
|
|
1967
|
-
/** Compiles derived-key rules (`{key}_plural`, `{key}_*`) into a function from a key to its base key. */
|
|
1968
|
-
function derivedBase(rules) {
|
|
1969
|
-
const regexes = rules.map((rule) => {
|
|
1970
|
-
const source = rule.split("{key}").map((part) => part.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, "[^.:]+")).join("(.+)");
|
|
1971
|
-
return new RegExp(`^${source}$`);
|
|
1972
|
-
});
|
|
1973
|
-
return (key) => {
|
|
1974
|
-
for (const re of regexes) {
|
|
1975
|
-
const match = re.exec(key);
|
|
1976
|
-
if (match?.[1]) return match[1];
|
|
1977
|
-
}
|
|
1978
|
-
};
|
|
1979
|
-
}
|
|
1980
|
-
//#endregion
|
|
1981
2083
|
//#region src/project/project.ts
|
|
1982
2084
|
const scanConcurrency = 64;
|
|
1983
2085
|
function createProject(options) {
|
|
@@ -1993,6 +2095,7 @@ function createProject(options) {
|
|
|
1993
2095
|
let revision = 0;
|
|
1994
2096
|
let namespaced = false;
|
|
1995
2097
|
let locales = [];
|
|
2098
|
+
let fallbacks = {};
|
|
1996
2099
|
let sourceLocale = config.sourceLocale;
|
|
1997
2100
|
let resolver;
|
|
1998
2101
|
let watcher;
|
|
@@ -2002,7 +2105,7 @@ function createProject(options) {
|
|
|
2002
2105
|
const normalizers = registry.plugins.flatMap((plugin) => plugin.normalizeKey ? [plugin.normalizeKey] : []);
|
|
2003
2106
|
const transforms = registry.plugins.flatMap((plugin) => plugin.transformResource ? [plugin.transformResource] : []);
|
|
2004
2107
|
const localeInfoHooks = registry.plugins.flatMap((plugin) => plugin.localeInfo ? [plugin.localeInfo] : []);
|
|
2005
|
-
const
|
|
2108
|
+
const { basesOf, formOf } = derivedKeys(config.usage.derivedKeys);
|
|
2006
2109
|
const scanSignature = hashText(JSON.stringify({
|
|
2007
2110
|
usage: config.usage,
|
|
2008
2111
|
namespace: config.namespace,
|
|
@@ -2030,6 +2133,11 @@ function createProject(options) {
|
|
|
2030
2133
|
}
|
|
2031
2134
|
return info;
|
|
2032
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
|
+
}
|
|
2033
2141
|
async function discover() {
|
|
2034
2142
|
const ctx = {
|
|
2035
2143
|
host,
|
|
@@ -2184,6 +2292,7 @@ function createProject(options) {
|
|
|
2184
2292
|
if (!found.includes(sourceLocale) && config.origins.sourceLocale === void 0 && found.length > 0) sourceLocale = found.find((code) => code.toLowerCase().startsWith("en")) ?? [...found].sort()[0];
|
|
2185
2293
|
locales = [sourceLocale, ...found.filter((code) => code !== sourceLocale).sort()];
|
|
2186
2294
|
if (!found.includes(sourceLocale) && found.length === 0) locales = [sourceLocale];
|
|
2295
|
+
fallbacks = resolveFallbacks(config.localeFallbacks, locales, sourceLocale);
|
|
2187
2296
|
resolver = createUsageResolver(config, namespaced, normalizers);
|
|
2188
2297
|
resolved = /* @__PURE__ */ new WeakMap();
|
|
2189
2298
|
resourceIndex = void 0;
|
|
@@ -2347,13 +2456,10 @@ function createProject(options) {
|
|
|
2347
2456
|
}
|
|
2348
2457
|
for (const { namespace, path } of keyPaths.values()) for (let i = 1; i < path.length; i++) parents.add(joinKey(namespace, path.slice(0, i)));
|
|
2349
2458
|
const derived = /* @__PURE__ */ new Map();
|
|
2350
|
-
if (config.usage.derivedKeys.length > 0) for (const key of messages.keys()) {
|
|
2351
|
-
const
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
derived.set(base, list);
|
|
2355
|
-
list.push(key);
|
|
2356
|
-
}
|
|
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);
|
|
2357
2463
|
}
|
|
2358
2464
|
return {
|
|
2359
2465
|
messages,
|
|
@@ -2425,8 +2531,7 @@ function createProject(options) {
|
|
|
2425
2531
|
const inPath = (usage) => query.path === void 0 || usage.path === query.path;
|
|
2426
2532
|
if (query.key !== void 0) {
|
|
2427
2533
|
const key = query.key;
|
|
2428
|
-
|
|
2429
|
-
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) => ({
|
|
2430
2535
|
usage,
|
|
2431
2536
|
possible: false
|
|
2432
2537
|
})), ...prefixUsages.filter((usage) => inPath(usage) && prefixMatches(usage, key)).map((usage) => ({
|
|
@@ -2456,6 +2561,7 @@ function createProject(options) {
|
|
|
2456
2561
|
revision,
|
|
2457
2562
|
sourceLocale,
|
|
2458
2563
|
locales,
|
|
2564
|
+
fallbacks,
|
|
2459
2565
|
resources: [...resourceStates.values()],
|
|
2460
2566
|
resourceErrors: [...resourceStates.values()].flatMap((s) => s.error ? [{
|
|
2461
2567
|
error: s.error,
|
|
@@ -2475,7 +2581,8 @@ function createProject(options) {
|
|
|
2475
2581
|
files,
|
|
2476
2582
|
history: history.get
|
|
2477
2583
|
}),
|
|
2478
|
-
|
|
2584
|
+
basesOf,
|
|
2585
|
+
formOf,
|
|
2479
2586
|
localeInfo,
|
|
2480
2587
|
text: (path) => fileTexts.get(path) ?? files.get(path)?.text
|
|
2481
2588
|
});
|
|
@@ -2489,7 +2596,7 @@ function createProject(options) {
|
|
|
2489
2596
|
const locale = query.missing;
|
|
2490
2597
|
list = list.filter((key) => {
|
|
2491
2598
|
const value = message(key, locale)?.value;
|
|
2492
|
-
return message(key, sourceLocale) !== void 0 && (value === void 0 || value === "");
|
|
2599
|
+
return message(key, sourceLocale) !== void 0 && (value === void 0 || value === "") && needs(key, locale);
|
|
2493
2600
|
});
|
|
2494
2601
|
}
|
|
2495
2602
|
if (query.unused) {
|
|
@@ -2509,7 +2616,7 @@ function createProject(options) {
|
|
|
2509
2616
|
namespace: id.namespace,
|
|
2510
2617
|
path: id.path,
|
|
2511
2618
|
messages: list,
|
|
2512
|
-
missing: locales.filter((locale) => !byLocale.has(locale)),
|
|
2619
|
+
missing: locales.filter((locale) => !byLocale.has(locale) && needs(key, locale)),
|
|
2513
2620
|
usages: usageMatches({ key }),
|
|
2514
2621
|
diagnostics: analyzer.analyze().diagnostics.filter((d) => d.key === key)
|
|
2515
2622
|
};
|
|
@@ -2630,6 +2737,7 @@ function createProject(options) {
|
|
|
2630
2737
|
revision,
|
|
2631
2738
|
sourceLocale,
|
|
2632
2739
|
locales,
|
|
2740
|
+
fallbacks,
|
|
2633
2741
|
namespaced,
|
|
2634
2742
|
resources: [...resourceStates.values()],
|
|
2635
2743
|
fileTexts
|
|
@@ -2640,7 +2748,7 @@ function createProject(options) {
|
|
|
2640
2748
|
parents: () => idx().parents,
|
|
2641
2749
|
usageMatches,
|
|
2642
2750
|
text: (path) => fileTexts.get(path) ?? files.get(path)?.text,
|
|
2643
|
-
|
|
2751
|
+
basesOf,
|
|
2644
2752
|
toStored: (target, path) => storedPathOf(target)(path),
|
|
2645
2753
|
resolveNew(locale, namespace) {
|
|
2646
2754
|
const ctx = {
|
|
@@ -2714,7 +2822,7 @@ function createProject(options) {
|
|
|
2714
2822
|
lookup(key, locale) {
|
|
2715
2823
|
const exact = message(key, locale);
|
|
2716
2824
|
if (exact) return exact;
|
|
2717
|
-
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));
|
|
2718
2826
|
const other = derived.find((k) => /[._-]other$/.test(k)) ?? derived[0];
|
|
2719
2827
|
return other === void 0 ? void 0 : message(other, locale);
|
|
2720
2828
|
},
|
|
@@ -2779,6 +2887,7 @@ function defaultDraft() {
|
|
|
2779
2887
|
sourceLocale: "en",
|
|
2780
2888
|
locales: void 0,
|
|
2781
2889
|
ignoreLocales: [],
|
|
2890
|
+
localeFallbacks: {},
|
|
2782
2891
|
resources: [],
|
|
2783
2892
|
formats: {},
|
|
2784
2893
|
keyStyle: "auto",
|
|
@@ -2822,6 +2931,12 @@ function serializablePart(config) {
|
|
|
2822
2931
|
history: options
|
|
2823
2932
|
};
|
|
2824
2933
|
}
|
|
2934
|
+
function mergeFallbacks(base, layer) {
|
|
2935
|
+
return base === "auto" || layer === "auto" ? layer : {
|
|
2936
|
+
...base,
|
|
2937
|
+
...layer
|
|
2938
|
+
};
|
|
2939
|
+
}
|
|
2825
2940
|
/** Applies a user config layer: scalars replace, lists of rules append. */
|
|
2826
2941
|
function applyLayer(draft, layer, origin, origins) {
|
|
2827
2942
|
const mark = (key) => {
|
|
@@ -2839,6 +2954,10 @@ function applyLayer(draft, layer, origin, origins) {
|
|
|
2839
2954
|
draft.ignoreLocales = [...draft.ignoreLocales, ...layer.ignoreLocales];
|
|
2840
2955
|
mark("ignoreLocales");
|
|
2841
2956
|
}
|
|
2957
|
+
if (layer.localeFallbacks !== void 0) {
|
|
2958
|
+
draft.localeFallbacks = mergeFallbacks(draft.localeFallbacks, layer.localeFallbacks);
|
|
2959
|
+
mark("localeFallbacks");
|
|
2960
|
+
}
|
|
2842
2961
|
if (layer.resources !== void 0) {
|
|
2843
2962
|
draft.resources = layer.resources;
|
|
2844
2963
|
mark("resources");
|
package/package.json
CHANGED
package/schema/config.json
CHANGED
|
@@ -25,6 +25,22 @@
|
|
|
25
25
|
"type": "string"
|
|
26
26
|
}
|
|
27
27
|
},
|
|
28
|
+
"localeFallbacks": {
|
|
29
|
+
"anyOf": [
|
|
30
|
+
{
|
|
31
|
+
"const": "auto"
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"type": "object",
|
|
35
|
+
"propertyNames": {
|
|
36
|
+
"type": "string"
|
|
37
|
+
},
|
|
38
|
+
"additionalProperties": {
|
|
39
|
+
"type": "string"
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
]
|
|
43
|
+
},
|
|
28
44
|
"presets": {
|
|
29
45
|
"anyOf": [
|
|
30
46
|
{
|
|
@@ -388,6 +404,12 @@
|
|
|
388
404
|
}
|
|
389
405
|
}
|
|
390
406
|
},
|
|
407
|
+
"attributes": {
|
|
408
|
+
"type": "array",
|
|
409
|
+
"items": {
|
|
410
|
+
"type": "string"
|
|
411
|
+
}
|
|
412
|
+
},
|
|
391
413
|
"ignoreAttributes": {
|
|
392
414
|
"type": "array",
|
|
393
415
|
"items": {
|