@x18n/core 0.0.9 → 0.0.12

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 CHANGED
@@ -575,6 +575,8 @@ interface CheckContext {
575
575
  readonly config: ResolvedConfig;
576
576
  readonly sourceLocale: LocaleCode;
577
577
  readonly locales: readonly LocaleCode[];
578
+ /** The locales a locale falls back to, nearest first. Empty for a locale without a fallback. */
579
+ readonly fallbacks: (locale: LocaleCode) => readonly LocaleCode[];
578
580
  readonly resources: readonly Resource[];
579
581
  /** Options from `[severity, options]` in config. */
580
582
  readonly options: JsonObject;
@@ -832,6 +834,10 @@ interface SerializableConfig {
832
834
  /** Locale codes as written in paths. Filters the found locales. */
833
835
  readonly locales?: readonly string[] | undefined;
834
836
  readonly ignoreLocales?: readonly string[] | undefined;
837
+ /** 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`. */
838
+ readonly localeFallbacks?: "auto" | {
839
+ readonly [locale: string]: string;
840
+ } | undefined;
835
841
  /** `'auto'` detects built-in presets from the project's dependencies. */
836
842
  readonly presets?: "auto" | readonly PluginSpecifier[] | undefined;
837
843
  readonly plugins?: readonly PluginSpecifier[] | undefined;
@@ -892,6 +898,9 @@ interface ResolvedConfig {
892
898
  readonly sourceLocale: string;
893
899
  readonly locales: readonly string[] | undefined;
894
900
  readonly ignoreLocales: readonly string[];
901
+ readonly localeFallbacks: "auto" | {
902
+ readonly [locale: string]: string;
903
+ };
895
904
  readonly resources: readonly ResourceSpec[];
896
905
  readonly formats: {
897
906
  readonly [extension: string]: string;
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",
@@ -712,6 +734,7 @@ const ConfigSchema = v.strictObject({
712
734
  sourceLocale: v.optional(v.string()),
713
735
  locales: v.optional(strings),
714
736
  ignoreLocales: v.optional(strings),
737
+ localeFallbacks: v.optional(v.union([v.literal("auto"), v.record(v.string(), v.string())])),
715
738
  presets: v.optional(v.union([v.literal("auto"), v.array(pluginSpecifier)])),
716
739
  plugins: v.optional(v.array(pluginSpecifier)),
717
740
  resources: v.optional(v.array(resourceSpec)),
@@ -930,14 +953,20 @@ function levelOf(setting) {
930
953
  function computeCoverage(state, reference) {
931
954
  const { messages, sourceLocale } = state;
932
955
  return state.locales.map((locale) => {
956
+ const chain = fallbackChain(state.fallbacks, locale);
957
+ const inherited = (key) => chain.some((code) => {
958
+ const value = messages.get(key)?.get(code)?.[0]?.value;
959
+ return value !== void 0 && value !== "";
960
+ });
933
961
  const missing = [];
934
962
  const empty = [];
935
963
  const outdated = [];
936
964
  for (const key of reference) {
937
965
  const value = messages.get(key)?.get(locale)?.[0]?.value;
938
- if (value === void 0) missing.push(key);
939
- else if (value === "") empty.push(key);
940
- else if (locale !== sourceLocale && isOutdated(state.history(key, sourceLocale), state.history(key, locale))) outdated.push(key);
966
+ if (value === void 0 || value === "") {
967
+ if (inherited(key)) continue;
968
+ (value === void 0 ? missing : empty).push(key);
969
+ } else if (locale !== sourceLocale && isOutdated(state.history(key, sourceLocale), state.history(key, locale))) outdated.push(key);
941
970
  }
942
971
  return {
943
972
  locale,
@@ -1069,6 +1098,7 @@ function createAnalyzer(options) {
1069
1098
  config,
1070
1099
  sourceLocale,
1071
1100
  locales,
1101
+ fallbacks: (locale) => fallbackChain(state.fallbacks, locale),
1072
1102
  resources: state.resources.map((s) => s.resource),
1073
1103
  hasScanners,
1074
1104
  keys: () => allKeys,
@@ -1543,7 +1573,7 @@ var PlanBuilder = class {
1543
1573
  case "set":
1544
1574
  this.setValue(op.key, op.locale, op.value);
1545
1575
  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, "");
1576
+ 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
1577
  }
1548
1578
  break;
1549
1579
  case "delete":
@@ -1993,6 +2023,7 @@ function createProject(options) {
1993
2023
  let revision = 0;
1994
2024
  let namespaced = false;
1995
2025
  let locales = [];
2026
+ let fallbacks = {};
1996
2027
  let sourceLocale = config.sourceLocale;
1997
2028
  let resolver;
1998
2029
  let watcher;
@@ -2184,6 +2215,7 @@ function createProject(options) {
2184
2215
  if (!found.includes(sourceLocale) && config.origins.sourceLocale === void 0 && found.length > 0) sourceLocale = found.find((code) => code.toLowerCase().startsWith("en")) ?? [...found].sort()[0];
2185
2216
  locales = [sourceLocale, ...found.filter((code) => code !== sourceLocale).sort()];
2186
2217
  if (!found.includes(sourceLocale) && found.length === 0) locales = [sourceLocale];
2218
+ fallbacks = resolveFallbacks(config.localeFallbacks, locales, sourceLocale);
2187
2219
  resolver = createUsageResolver(config, namespaced, normalizers);
2188
2220
  resolved = /* @__PURE__ */ new WeakMap();
2189
2221
  resourceIndex = void 0;
@@ -2456,6 +2488,7 @@ function createProject(options) {
2456
2488
  revision,
2457
2489
  sourceLocale,
2458
2490
  locales,
2491
+ fallbacks,
2459
2492
  resources: [...resourceStates.values()],
2460
2493
  resourceErrors: [...resourceStates.values()].flatMap((s) => s.error ? [{
2461
2494
  error: s.error,
@@ -2630,6 +2663,7 @@ function createProject(options) {
2630
2663
  revision,
2631
2664
  sourceLocale,
2632
2665
  locales,
2666
+ fallbacks,
2633
2667
  namespaced,
2634
2668
  resources: [...resourceStates.values()],
2635
2669
  fileTexts
@@ -2779,6 +2813,7 @@ function defaultDraft() {
2779
2813
  sourceLocale: "en",
2780
2814
  locales: void 0,
2781
2815
  ignoreLocales: [],
2816
+ localeFallbacks: {},
2782
2817
  resources: [],
2783
2818
  formats: {},
2784
2819
  keyStyle: "auto",
@@ -2822,6 +2857,12 @@ function serializablePart(config) {
2822
2857
  history: options
2823
2858
  };
2824
2859
  }
2860
+ function mergeFallbacks(base, layer) {
2861
+ return base === "auto" || layer === "auto" ? layer : {
2862
+ ...base,
2863
+ ...layer
2864
+ };
2865
+ }
2825
2866
  /** Applies a user config layer: scalars replace, lists of rules append. */
2826
2867
  function applyLayer(draft, layer, origin, origins) {
2827
2868
  const mark = (key) => {
@@ -2839,6 +2880,10 @@ function applyLayer(draft, layer, origin, origins) {
2839
2880
  draft.ignoreLocales = [...draft.ignoreLocales, ...layer.ignoreLocales];
2840
2881
  mark("ignoreLocales");
2841
2882
  }
2883
+ if (layer.localeFallbacks !== void 0) {
2884
+ draft.localeFallbacks = mergeFallbacks(draft.localeFallbacks, layer.localeFallbacks);
2885
+ mark("localeFallbacks");
2886
+ }
2842
2887
  if (layer.resources !== void 0) {
2843
2888
  draft.resources = layer.resources;
2844
2889
  mark("resources");
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@x18n/core",
3
3
  "type": "module",
4
- "version": "0.0.9",
4
+ "version": "0.0.12",
5
5
  "description": "Runtime-neutral engine of x18n: domain model, plugin API and project analysis",
6
6
  "author": "Anthony Fu <anthonyfu117@hotmail.com>",
7
7
  "license": "MIT",
@@ -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
  {