aboutsummaryrefslogtreecommitdiff
path: root/src/locale-proxy.ts
blob: a4dcc01843e8b9d4aa0f9dc7cbdcb945b0406920 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
import type { LocaleDefinition } from './definitions';
import { FakerError } from './errors/faker-error';

/**
 * A proxy for LocaleDefinition that marks all properties as required and throws an error when an entry is accessed that is not defined.
 */
export type LocaleProxy = Readonly<{
  [key in keyof LocaleDefinition]-?: LocaleProxyCategory<LocaleDefinition[key]>;
}>;

type LocaleProxyCategory<T> = Readonly<{
  [key in keyof T]-?: LocaleProxyEntry<T[key]>;
}>;

type LocaleProxyEntry<T> = unknown extends T ? T : Readonly<NonNullable<T>>;

const throwReadOnlyError: () => never = () => {
  throw new FakerError('You cannot edit the locale data on the faker instance');
};

/**
 * Creates a proxy for LocaleDefinition that throws an error if an undefined property is accessed.
 *
 * @param locale The locale definition to create the proxy for.
 */
export function createLocaleProxy(locale: LocaleDefinition): LocaleProxy {
  const proxies = {} as LocaleDefinition;
  return new Proxy(locale, {
    has(): true {
      // Categories are always present (proxied), that's why we return true.
      return true;
    },

    get(
      target: LocaleDefinition,
      categoryName: keyof LocaleDefinition
    ): LocaleDefinition[keyof LocaleDefinition] {
      if (typeof categoryName === 'symbol' || categoryName === 'nodeType') {
        return target[categoryName];
      }

      if (categoryName in proxies) {
        return proxies[categoryName];
      }

      return (proxies[categoryName] = createCategoryProxy(
        categoryName,
        target[categoryName]
      ));
    },

    set: throwReadOnlyError,
    deleteProperty: throwReadOnlyError,
  }) as LocaleProxy;
}

/**
 * Creates a proxy for a category that throws an error when accessing an undefined property.
 *
 * @param categoryName The name of the category.
 * @param categoryData The module to create the proxy for.
 */
function createCategoryProxy<
  CategoryData extends Record<string | symbol, unknown>
>(
  categoryName: string,
  categoryData: CategoryData = {} as CategoryData
): Required<CategoryData> {
  return new Proxy(categoryData, {
    has(target: CategoryData, entryName: keyof CategoryData): boolean {
      const value = target[entryName];
      return value != null;
    },

    get(
      target: CategoryData,
      entryName: keyof CategoryData
    ): CategoryData[keyof CategoryData] {
      const value = target[entryName];
      if (typeof entryName === 'symbol' || entryName === 'nodeType') {
        return value;
      } else if (value === null) {
        throw new FakerError(
          `The locale data for '${categoryName}.${entryName.toString()}' aren't applicable to this locale.
  If you think this is a bug, please report it at: https://github.com/faker-js/faker`
        );
      } else if (value === undefined) {
        throw new FakerError(
          `The locale data for '${categoryName}.${entryName.toString()}' are missing in this locale.
  Please contribute the missing data to the project or use a locale/Faker instance that has these data.
  For more information see https://next.fakerjs.dev/guide/localization.html`
        );
      } else {
        return value;
      }
    },

    set: throwReadOnlyError,
    deleteProperty: throwReadOnlyError,
  }) as Required<CategoryData>;
}