@discord/intl 0.19.1 → 0.19.2

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.ts CHANGED
@@ -6,6 +6,7 @@ export { runtimeHashMessageKey } from './hash';
6
6
  export { IntlManager, DEFAULT_LOCALE, type FormatFunction } from './intl-manager';
7
7
  export { createLoader, loadAllMessagesInLocale, waitForAllDefaultIntlMessagesLoaded, MessageLoader, } from './message-loader';
8
8
  export type * from './types.d.ts';
9
+ export { chainMessagesObjects, makeMessagesProxy } from './runtime-utils';
9
10
  /**
10
11
  * The return value of `formatToParts` from `@discord/intl`, this type
11
12
  * represents any AST structure for a message rendered using this system.
package/dist/index.js CHANGED
@@ -14,7 +14,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
14
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
15
  };
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
- exports.MessageLoader = exports.waitForAllDefaultIntlMessagesLoaded = exports.loadAllMessagesInLocale = exports.createLoader = exports.DEFAULT_LOCALE = exports.IntlManager = exports.runtimeHashMessageKey = exports.bindFormatValues = exports.FormatBuilder = exports.dataFormatterCache = exports.makeDataFormatters = void 0;
17
+ exports.makeMessagesProxy = exports.chainMessagesObjects = exports.MessageLoader = exports.waitForAllDefaultIntlMessagesLoaded = exports.loadAllMessagesInLocale = exports.createLoader = exports.DEFAULT_LOCALE = exports.IntlManager = exports.runtimeHashMessageKey = exports.bindFormatValues = exports.FormatBuilder = exports.dataFormatterCache = exports.makeDataFormatters = void 0;
18
18
  var data_formatters_1 = require("./data-formatters");
19
19
  Object.defineProperty(exports, "makeDataFormatters", { enumerable: true, get: function () { return data_formatters_1.makeDataFormatters; } });
20
20
  var cache_1 = require("./data-formatters/cache");
@@ -33,3 +33,6 @@ Object.defineProperty(exports, "createLoader", { enumerable: true, get: function
33
33
  Object.defineProperty(exports, "loadAllMessagesInLocale", { enumerable: true, get: function () { return message_loader_1.loadAllMessagesInLocale; } });
34
34
  Object.defineProperty(exports, "waitForAllDefaultIntlMessagesLoaded", { enumerable: true, get: function () { return message_loader_1.waitForAllDefaultIntlMessagesLoaded; } });
35
35
  Object.defineProperty(exports, "MessageLoader", { enumerable: true, get: function () { return message_loader_1.MessageLoader; } });
36
+ var runtime_utils_1 = require("./runtime-utils");
37
+ Object.defineProperty(exports, "chainMessagesObjects", { enumerable: true, get: function () { return runtime_utils_1.chainMessagesObjects; } });
38
+ Object.defineProperty(exports, "makeMessagesProxy", { enumerable: true, get: function () { return runtime_utils_1.makeMessagesProxy; } });
@@ -52,9 +52,28 @@ export declare class MessageLoader {
52
52
  */
53
53
  _subscribers: Set<() => void>;
54
54
  /**
55
- * Message to show as a fallback when the requested message is unavailable.
55
+ * If a message is not present in this loader, try looking it up in this
56
+ * loader instead before resorting to a default placeholder message.
57
+ *
58
+ * The order of lookups will be:
59
+ * - this loader, requested locale
60
+ * - this loader, default locale
61
+ * - fallback loader, requested locale
62
+ * - fallback loader, default locale
63
+ * - fallback message
64
+ */
65
+ fallbackLoader?: MessageLoader;
66
+ /**
67
+ * Message to show as a fallback when the requested message is unavailable
68
+ * from both this loader and the fallback loader.
56
69
  */
57
70
  fallbackMessage: InternalIntlMessage;
71
+ /**
72
+ * When `fallbackLoader` is set, this value will be set on the fallback
73
+ * loader to reference this loader. Setting this ensures that loaders won't
74
+ * ever form an infinite circular chain.
75
+ */
76
+ _parentLoader?: MessageLoader;
58
77
  /**
59
78
  * Map from hashed message keys to their original values, to provide context in error messages
60
79
  * that would otherwise be obfuscated.
@@ -82,6 +101,23 @@ export declare class MessageLoader {
82
101
  * @param {Record<string, string>} localeFileMap
83
102
  */
84
103
  withDebugValues(keyMap: Record<string, string>, localeFileMap: Record<string, string>): void;
104
+ /**
105
+ * Configure a fallback loader to search for any message requests that are not resolved by this
106
+ * loader directly. For example, if a request for a message "abcdef" in locale `pt-BR` is not
107
+ * found in this loader's managed content, the request gets forwarded on to this fallback instead.
108
+ *
109
+ * The lookup first exhausts this loader entirely before checking the fallback, in the order:
110
+ * - This loader's requested locale.
111
+ * - This loader's default locale.
112
+ * - Fallback loader's requested locale.
113
+ * - Fallback loader's default locale.
114
+ * - Fallback loader's final fallback value.
115
+ * - This loader's final fallback value.
116
+ *
117
+ * Note that the final fallback value (the `fallbackMessage` property on this class) will first
118
+ * be retrieved from the fallback loader rather than this instance.
119
+ */
120
+ fallbackWith(fallback: MessageLoader): void;
85
121
  get(key: string, locale: LocaleId): InternalIntlMessage;
86
122
  /**
87
123
  * Return the value of the message with the given `key` in the given `locale`. If the message has
@@ -50,7 +50,35 @@ class MessageLoader {
50
50
  this._debugKeyMap = keyMap;
51
51
  this._localeFileMap = localeFileMap;
52
52
  }
53
+ /**
54
+ * Configure a fallback loader to search for any message requests that are not resolved by this
55
+ * loader directly. For example, if a request for a message "abcdef" in locale `pt-BR` is not
56
+ * found in this loader's managed content, the request gets forwarded on to this fallback instead.
57
+ *
58
+ * The lookup first exhausts this loader entirely before checking the fallback, in the order:
59
+ * - This loader's requested locale.
60
+ * - This loader's default locale.
61
+ * - Fallback loader's requested locale.
62
+ * - Fallback loader's default locale.
63
+ * - Fallback loader's final fallback value.
64
+ * - This loader's final fallback value.
65
+ *
66
+ * Note that the final fallback value (the `fallbackMessage` property on this class) will first
67
+ * be retrieved from the fallback loader rather than this instance.
68
+ */
69
+ fallbackWith(fallback) {
70
+ let parent = this;
71
+ while (parent != null) {
72
+ parent = parent._parentLoader;
73
+ if (parent === this) {
74
+ throw new Error('Setting `fallbackWith` on MessageLoader created a circular chain that would never resolve');
75
+ }
76
+ }
77
+ this.fallbackLoader = fallback;
78
+ fallback._parentLoader = this;
79
+ }
53
80
  get(key, locale) {
81
+ var _a;
54
82
  const expectedValue = this.getMessageValue(key, locale);
55
83
  if (expectedValue != null)
56
84
  return expectedValue;
@@ -60,9 +88,13 @@ class MessageLoader {
60
88
  if (this.isLocaleLoading(locale) && !this.isLocaleLoaded(this.defaultLocale)) {
61
89
  return this.fallbackMessage;
62
90
  }
63
- const fallbackValue = this.getMessageValue(key, this.defaultLocale);
64
- if (fallbackValue != null)
65
- return fallbackValue;
91
+ const defaultLocaleValue = this.getMessageValue(key, this.defaultLocale);
92
+ if (defaultLocaleValue != null)
93
+ return defaultLocaleValue;
94
+ const fallbackLoaderValue = (_a = this.fallbackLoader) === null || _a === void 0 ? void 0 : _a.get(key, locale);
95
+ if (fallbackLoaderValue != null) {
96
+ return fallbackLoaderValue;
97
+ }
66
98
  // If the message couldn't be found in either the requested nor the default locale, then
67
99
  // nothing can be done.
68
100
  const errorKey = this._debugKeyMap != null ? `"${this._debugKeyMap[key]}" (${key})` : undefined;
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Utility functions and classes used at runtime by the compiled output of bundlers using the intl
3
+ * packages like `intl-loader-core`'s `MessageDefinitionsTransformer`.
4
+ */
5
+ import { MessageLoader } from './message-loader';
6
+ import { IntlMessageGetter } from './types';
7
+ interface MessageBindsProxy {
8
+ $$baseObject: Record<string, IntlMessageGetter>;
9
+ $$loader: MessageLoader;
10
+ }
11
+ /** Type created by the message transformer when `bindMode` is set to `literal`. */
12
+ export type MessagesLiteral<T extends string> = Record<T, IntlMessageGetter>;
13
+ export type AnyIntlMessagesObject<T extends string> = MessageBindsProxy | MessagesLiteral<T>;
14
+ /**
15
+ * Return a new value that represents two message objects combined into one, regardless of their
16
+ * kind (either proxies or object literals). The returned value will be able to access any message
17
+ * contained in either of the two objects. This method generally assumes there is no overlap in
18
+ * keys between the two message objects, and the order of resolution is undefined (varies based on
19
+ * the types of the objects).
20
+ *
21
+ * Note that if both objects are proxies, the base loaders _are mutated in place_ to be chained
22
+ * together. Any other usages of the first object will also automatically fall back to the second.
23
+ */
24
+ export declare function chainMessagesObjects<const First extends MessagesLiteral<string>, const Second extends MessagesLiteral<string>>(first: First, second: Second): First & Second;
25
+ export declare function makeMessagesProxy(loader: MessageLoader): Record<string, IntlMessageGetter>;
26
+ export {};
@@ -0,0 +1,90 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.chainMessagesObjects = chainMessagesObjects;
4
+ exports.makeMessagesProxy = makeMessagesProxy;
5
+ function isMessagesProxy(object) {
6
+ return object[Symbol.toStringTag] === 'IntlMessagesProxy';
7
+ }
8
+ /**
9
+ * Return a new value that represents two message objects combined into one, regardless of their
10
+ * kind (either proxies or object literals). The returned value will be able to access any message
11
+ * contained in either of the two objects. This method generally assumes there is no overlap in
12
+ * keys between the two message objects, and the order of resolution is undefined (varies based on
13
+ * the types of the objects).
14
+ *
15
+ * Note that if both objects are proxies, the base loaders _are mutated in place_ to be chained
16
+ * together. Any other usages of the first object will also automatically fall back to the second.
17
+ */
18
+ function chainMessagesObjects(first, second) {
19
+ const firstIsProxy = isMessagesProxy(first);
20
+ const secondIsProxy = isMessagesProxy(second);
21
+ // This explicit any is a little strange, but when the objects are proxies, they don't have any
22
+ // actual type information, so trying to return them as a combination of `First` and `Second`
23
+ // causes TypeScript to error saying there are possibly different instantiations. Casting to `any`
24
+ // first makes it not care.
25
+ let result = first;
26
+ if (firstIsProxy && secondIsProxy) {
27
+ // If both are proxies, no change is actually required, and the first proxy can just have its
28
+ // loader set to fall back to the second.
29
+ first.$$loader.fallbackWith(second.$$loader);
30
+ result = first;
31
+ }
32
+ else if (!firstIsProxy && !secondIsProxy) {
33
+ // If both objects are plain literals, they can just be spread together to get the result.
34
+ result = Object.assign(Object.assign({}, second), first);
35
+ }
36
+ else if (firstIsProxy && !secondIsProxy) {
37
+ // If the first is a proxy and the second is an object, the second can be spread onto the first
38
+ // to "pre-fill" values on it. In reality these cases should never be hit.
39
+ result = Object.assign(first.$$baseObject, second);
40
+ }
41
+ else if (secondIsProxy && !firstIsProxy) {
42
+ // And the same is true in reverse.
43
+ result = Object.assign(second.$$baseObject, first);
44
+ }
45
+ return result;
46
+ }
47
+ function makeMessagesProxy(loader) {
48
+ function makeBind(prop) {
49
+ return (locale) => loader.get(prop, locale);
50
+ }
51
+ const baseObject = {};
52
+ const proxy = new Proxy(baseObject, {
53
+ ownKeys(self) {
54
+ return Reflect.ownKeys(self);
55
+ },
56
+ getOwnPropertyDescriptor(self, prop) {
57
+ return {
58
+ value: (self[prop] || (self[prop] = makeBind(prop))),
59
+ configurable: true,
60
+ enumerable: true,
61
+ writable: false,
62
+ };
63
+ },
64
+ get(self, prop) {
65
+ if (prop === '$$typeof') {
66
+ return 'object';
67
+ }
68
+ if (prop === Symbol.toStringTag) {
69
+ return 'IntlMessagesProxy';
70
+ }
71
+ self[prop] || (self[prop] = makeBind(prop));
72
+ return self[prop];
73
+ },
74
+ });
75
+ // Define the base object and loader on the proxy directly, but make them non-enumerable so that
76
+ // they don't show up when using `Object.keys` or other accessors.
77
+ Object.defineProperty(proxy, '$$baseObject', {
78
+ value: baseObject,
79
+ enumerable: false,
80
+ configurable: false,
81
+ writable: false,
82
+ });
83
+ Object.defineProperty(proxy, '$$loader', {
84
+ value: loader,
85
+ enumerable: false,
86
+ configurable: false,
87
+ writable: false,
88
+ });
89
+ return proxy;
90
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@discord/intl",
3
- "version": "0.19.1",
3
+ "version": "0.19.2",
4
4
  "license": "MIT",
5
5
  "description": "Client runtime for managing messages and translations in a React project.",
6
6
  "main": "./dist/index.js",
@@ -23,7 +23,7 @@
23
23
  "@formatjs/intl": "^2.10.1",
24
24
  "@intrnl/xxhash64": "^0.1.2",
25
25
  "intl-messageformat": "^10.5.11",
26
- "@discord/intl-ast": "0.19.1"
26
+ "@discord/intl-ast": "0.19.2"
27
27
  },
28
28
  "devDependencies": {
29
29
  "@formatjs/intl-durationformat": "^0.7.3",
package/src/index.ts CHANGED
@@ -12,6 +12,8 @@ export {
12
12
  } from './message-loader';
13
13
  export type * from './types.d.ts';
14
14
 
15
+ export { chainMessagesObjects, makeMessagesProxy } from './runtime-utils';
16
+
15
17
  /**
16
18
  * The return value of `formatToParts` from `@discord/intl`, this type
17
19
  * represents any AST structure for a message rendered using this system.
@@ -58,10 +58,30 @@ export class MessageLoader {
58
58
  _subscribers: Set<() => void>;
59
59
 
60
60
  /**
61
- * Message to show as a fallback when the requested message is unavailable.
61
+ * If a message is not present in this loader, try looking it up in this
62
+ * loader instead before resorting to a default placeholder message.
63
+ *
64
+ * The order of lookups will be:
65
+ * - this loader, requested locale
66
+ * - this loader, default locale
67
+ * - fallback loader, requested locale
68
+ * - fallback loader, default locale
69
+ * - fallback message
70
+ */
71
+ fallbackLoader?: MessageLoader;
72
+ /**
73
+ * Message to show as a fallback when the requested message is unavailable
74
+ * from both this loader and the fallback loader.
62
75
  */
63
76
  fallbackMessage: InternalIntlMessage;
64
77
 
78
+ /**
79
+ * When `fallbackLoader` is set, this value will be set on the fallback
80
+ * loader to reference this loader. Setting this ensures that loaders won't
81
+ * ever form an infinite circular chain.
82
+ */
83
+ _parentLoader?: MessageLoader;
84
+
65
85
  ///
66
86
  // Debug mode values
67
87
  ///
@@ -125,6 +145,36 @@ export class MessageLoader {
125
145
  this._localeFileMap = localeFileMap;
126
146
  }
127
147
 
148
+ /**
149
+ * Configure a fallback loader to search for any message requests that are not resolved by this
150
+ * loader directly. For example, if a request for a message "abcdef" in locale `pt-BR` is not
151
+ * found in this loader's managed content, the request gets forwarded on to this fallback instead.
152
+ *
153
+ * The lookup first exhausts this loader entirely before checking the fallback, in the order:
154
+ * - This loader's requested locale.
155
+ * - This loader's default locale.
156
+ * - Fallback loader's requested locale.
157
+ * - Fallback loader's default locale.
158
+ * - Fallback loader's final fallback value.
159
+ * - This loader's final fallback value.
160
+ *
161
+ * Note that the final fallback value (the `fallbackMessage` property on this class) will first
162
+ * be retrieved from the fallback loader rather than this instance.
163
+ */
164
+ fallbackWith(fallback: MessageLoader) {
165
+ let parent: MessageLoader = this;
166
+ while (parent != null) {
167
+ parent = parent._parentLoader;
168
+ if (parent === this) {
169
+ throw new Error(
170
+ 'Setting `fallbackWith` on MessageLoader created a circular chain that would never resolve',
171
+ );
172
+ }
173
+ }
174
+ this.fallbackLoader = fallback;
175
+ fallback._parentLoader = this;
176
+ }
177
+
128
178
  get(key: string, locale: LocaleId): InternalIntlMessage {
129
179
  const expectedValue = this.getMessageValue(key, locale);
130
180
  if (expectedValue != null) return expectedValue;
@@ -135,8 +185,13 @@ export class MessageLoader {
135
185
  return this.fallbackMessage;
136
186
  }
137
187
 
138
- const fallbackValue = this.getMessageValue(key, this.defaultLocale);
139
- if (fallbackValue != null) return fallbackValue;
188
+ const defaultLocaleValue = this.getMessageValue(key, this.defaultLocale);
189
+ if (defaultLocaleValue != null) return defaultLocaleValue;
190
+
191
+ const fallbackLoaderValue = this.fallbackLoader?.get(key, locale);
192
+ if (fallbackLoaderValue != null) {
193
+ return fallbackLoaderValue;
194
+ }
140
195
 
141
196
  // If the message couldn't be found in either the requested nor the default locale, then
142
197
  // nothing can be done.
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Utility functions and classes used at runtime by the compiled output of bundlers using the intl
3
+ * packages like `intl-loader-core`'s `MessageDefinitionsTransformer`.
4
+ */
5
+ import { MessageLoader } from './message-loader';
6
+ import { IntlMessageGetter } from './types';
7
+
8
+ interface MessageBindsProxy {
9
+ $$baseObject: Record<string, IntlMessageGetter>;
10
+ $$loader: MessageLoader;
11
+ }
12
+
13
+ /** Type created by the message transformer when `bindMode` is set to `literal`. */
14
+ export type MessagesLiteral<T extends string> = Record<T, IntlMessageGetter>;
15
+ export type AnyIntlMessagesObject<T extends string> = MessageBindsProxy | MessagesLiteral<T>;
16
+
17
+ function isMessagesProxy(object: AnyIntlMessagesObject<any>): object is MessageBindsProxy {
18
+ return object[Symbol.toStringTag] === 'IntlMessagesProxy';
19
+ }
20
+
21
+ /**
22
+ * Return a new value that represents two message objects combined into one, regardless of their
23
+ * kind (either proxies or object literals). The returned value will be able to access any message
24
+ * contained in either of the two objects. This method generally assumes there is no overlap in
25
+ * keys between the two message objects, and the order of resolution is undefined (varies based on
26
+ * the types of the objects).
27
+ *
28
+ * Note that if both objects are proxies, the base loaders _are mutated in place_ to be chained
29
+ * together. Any other usages of the first object will also automatically fall back to the second.
30
+ */
31
+ export function chainMessagesObjects<
32
+ const First extends MessagesLiteral<string>,
33
+ const Second extends MessagesLiteral<string>,
34
+ >(first: First, second: Second): First & Second {
35
+ const firstIsProxy = isMessagesProxy(first);
36
+ const secondIsProxy = isMessagesProxy(second);
37
+
38
+ // This explicit any is a little strange, but when the objects are proxies, they don't have any
39
+ // actual type information, so trying to return them as a combination of `First` and `Second`
40
+ // causes TypeScript to error saying there are possibly different instantiations. Casting to `any`
41
+ // first makes it not care.
42
+ let result: any = first;
43
+
44
+ if (firstIsProxy && secondIsProxy) {
45
+ // If both are proxies, no change is actually required, and the first proxy can just have its
46
+ // loader set to fall back to the second.
47
+ first.$$loader.fallbackWith(second.$$loader);
48
+ result = first;
49
+ } else if (!firstIsProxy && !secondIsProxy) {
50
+ // If both objects are plain literals, they can just be spread together to get the result.
51
+ result = { ...second, ...first };
52
+ } else if (firstIsProxy && !secondIsProxy) {
53
+ // If the first is a proxy and the second is an object, the second can be spread onto the first
54
+ // to "pre-fill" values on it. In reality these cases should never be hit.
55
+ result = Object.assign(first.$$baseObject, second);
56
+ } else if (secondIsProxy && !firstIsProxy) {
57
+ // And the same is true in reverse.
58
+ result = Object.assign(second.$$baseObject, first);
59
+ }
60
+
61
+ return result as First & Second;
62
+ }
63
+
64
+ export function makeMessagesProxy(loader: MessageLoader): Record<string, IntlMessageGetter> {
65
+ function makeBind(prop: string) {
66
+ return (locale: string) => loader.get(prop, locale);
67
+ }
68
+
69
+ const baseObject = {};
70
+ const proxy = new Proxy(baseObject, {
71
+ ownKeys(self) {
72
+ return Reflect.ownKeys(self);
73
+ },
74
+ getOwnPropertyDescriptor(self, prop) {
75
+ return {
76
+ value: (self[prop] ||= makeBind(prop as string)),
77
+ configurable: true,
78
+ enumerable: true,
79
+ writable: false,
80
+ };
81
+ },
82
+ get(self, prop) {
83
+ if (prop === '$$typeof') {
84
+ return 'object';
85
+ }
86
+ if (prop === Symbol.toStringTag) {
87
+ return 'IntlMessagesProxy';
88
+ }
89
+
90
+ self[prop] ||= makeBind(prop as string);
91
+ return self[prop];
92
+ },
93
+ });
94
+
95
+ // Define the base object and loader on the proxy directly, but make them non-enumerable so that
96
+ // they don't show up when using `Object.keys` or other accessors.
97
+ Object.defineProperty(proxy, '$$baseObject', {
98
+ value: baseObject,
99
+ enumerable: false,
100
+ configurable: false,
101
+ writable: false,
102
+ });
103
+ Object.defineProperty(proxy, '$$loader', {
104
+ value: loader,
105
+ enumerable: false,
106
+ configurable: false,
107
+ writable: false,
108
+ });
109
+ return proxy;
110
+ }