@discord/intl 0.18.0 → 0.19.1-canary.f6592a8
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 +1 -0
- package/dist/index.js +4 -1
- package/dist/message-loader.d.ts +45 -20
- package/dist/message-loader.js +54 -33
- package/dist/runtime-utils.d.ts +26 -0
- package/dist/runtime-utils.js +90 -0
- package/package.json +2 -2
- package/src/index.ts +2 -0
- package/src/message-loader.ts +77 -39
- package/src/runtime-utils.ts +110 -0
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; } });
|
package/dist/message-loader.d.ts
CHANGED
|
@@ -52,9 +52,28 @@ export declare class MessageLoader {
|
|
|
52
52
|
*/
|
|
53
53
|
_subscribers: Set<() => void>;
|
|
54
54
|
/**
|
|
55
|
-
*
|
|
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.
|
|
@@ -73,7 +92,7 @@ export declare class MessageLoader {
|
|
|
73
92
|
* @private
|
|
74
93
|
*/
|
|
75
94
|
_localeFileMap?: Record<string, string>;
|
|
76
|
-
constructor(
|
|
95
|
+
constructor(localeImportMap: LocaleImportMap, defaultLocale: LocaleId);
|
|
77
96
|
/**
|
|
78
97
|
* Provide additional debug information to use during development, providing additional context
|
|
79
98
|
* for console error messages.
|
|
@@ -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
|
|
@@ -93,20 +129,6 @@ export declare class MessageLoader {
|
|
|
93
129
|
* returning.
|
|
94
130
|
*/
|
|
95
131
|
getMessageValue(key: string, locale: LocaleId): InternalIntlMessage | undefined;
|
|
96
|
-
/**
|
|
97
|
-
* Returns a record mapping the keys this object manages to bound functions
|
|
98
|
-
* for `get` with the that key as the first argument, allowing consumers to
|
|
99
|
-
* just call the function with a locale to retrieve the translated message
|
|
100
|
-
* for that key.
|
|
101
|
-
*
|
|
102
|
-
* This method is provided as a way to generate binds _at runtime_, but for
|
|
103
|
-
* very-large messages files (e.g., thousands of messages), this can be a
|
|
104
|
-
* non-negligible cost. Where feasible, consider generating these binds
|
|
105
|
-
* at build/bundle time, especially in cases where they can be substantially
|
|
106
|
-
* minified (i.e., Hermes bytecode). This is provided automatically as an
|
|
107
|
-
* option when using one of `@discord/intl`'s transformers.
|
|
108
|
-
*/
|
|
109
|
-
getBinds(): Record<string, IntlMessageGetter>;
|
|
110
132
|
_loadLocale(locale: LocaleId): Promise<void>;
|
|
111
133
|
/**
|
|
112
134
|
* Inform subscribers that the loader state has changed and they should
|
|
@@ -145,9 +167,12 @@ export declare function loadAllMessagesInLocale(locale: LocaleId): Promise<void>
|
|
|
145
167
|
*/
|
|
146
168
|
export declare function waitForAllDefaultIntlMessagesLoaded(): Promise<void>;
|
|
147
169
|
/**
|
|
148
|
-
* Create a new MessageLoader, which handles lazily loading messages for
|
|
149
|
-
*
|
|
150
|
-
*
|
|
170
|
+
* Create a new MessageLoader, which handles lazily loading messages for different locales and
|
|
171
|
+
* sanity checks as needed to provide accessors for each message contained by the import map.
|
|
172
|
+
*
|
|
173
|
+
* Notably, this does _not_ verify whether the locale objects managed by this loader actually
|
|
174
|
+
* contain a given key. It is the responsibility of the caller to verify this (easily enforced with
|
|
175
|
+
* typescript, eslint rules, and others).
|
|
151
176
|
*/
|
|
152
|
-
export declare function createLoader(
|
|
177
|
+
export declare function createLoader(localeImportMap: LocaleImportMap, defaultLocale: LocaleId): MessageLoader;
|
|
153
178
|
export {};
|
package/dist/message-loader.js
CHANGED
|
@@ -15,8 +15,7 @@ exports.waitForAllDefaultIntlMessagesLoaded = waitForAllDefaultIntlMessagesLoade
|
|
|
15
15
|
exports.createLoader = createLoader;
|
|
16
16
|
const message_1 = require("./message");
|
|
17
17
|
class MessageLoader {
|
|
18
|
-
constructor(
|
|
19
|
-
this.messageKeys = messageKeys;
|
|
18
|
+
constructor(localeImportMap, defaultLocale) {
|
|
20
19
|
this.messages = {};
|
|
21
20
|
this.localeImportMap = localeImportMap;
|
|
22
21
|
this.supportedLocales = Object.keys(localeImportMap);
|
|
@@ -51,7 +50,35 @@ class MessageLoader {
|
|
|
51
50
|
this._debugKeyMap = keyMap;
|
|
52
51
|
this._localeFileMap = localeFileMap;
|
|
53
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
|
+
}
|
|
54
80
|
get(key, locale) {
|
|
81
|
+
var _a;
|
|
55
82
|
const expectedValue = this.getMessageValue(key, locale);
|
|
56
83
|
if (expectedValue != null)
|
|
57
84
|
return expectedValue;
|
|
@@ -61,9 +88,13 @@ class MessageLoader {
|
|
|
61
88
|
if (this.isLocaleLoading(locale) && !this.isLocaleLoaded(this.defaultLocale)) {
|
|
62
89
|
return this.fallbackMessage;
|
|
63
90
|
}
|
|
64
|
-
const
|
|
65
|
-
if (
|
|
66
|
-
return
|
|
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
|
+
}
|
|
67
98
|
// If the message couldn't be found in either the requested nor the default locale, then
|
|
68
99
|
// nothing can be done.
|
|
69
100
|
const errorKey = this._debugKeyMap != null ? `"${this._debugKeyMap[key]}" (${key})` : undefined;
|
|
@@ -99,9 +130,16 @@ class MessageLoader {
|
|
|
99
130
|
}
|
|
100
131
|
return undefined;
|
|
101
132
|
}
|
|
102
|
-
// Then try to return the loaded message.
|
|
103
|
-
|
|
104
|
-
|
|
133
|
+
// Then try to return the loaded message. Previously, this used a `key in ...` check to quickly
|
|
134
|
+
// verify whether the locale object contains the requested message, but in the case where the
|
|
135
|
+
// target is a Proxy or uses some other internalized method for providing gettable values, `in`
|
|
136
|
+
// queries aren't guaranteed to work. Instead, since this is realistically amortized to just be
|
|
137
|
+
// a property access on every call, we can just the returned value as the determining factor. If
|
|
138
|
+
// the locale object returns `undefined`, then it "does not contain" the message, whether it's
|
|
139
|
+
// still loading or otherwise. Future calls will then be able to pick up any changed value,
|
|
140
|
+
// since it won't be cached in the `_parseCache`.
|
|
141
|
+
const content = this.messages[locale][key];
|
|
142
|
+
if (content != null) {
|
|
105
143
|
const message = new message_1.InternalIntlMessage(content, locale);
|
|
106
144
|
((_b = (_c = this._parseCache)[locale]) !== null && _b !== void 0 ? _b : (_c[locale] = {}))[key] = message;
|
|
107
145
|
return message;
|
|
@@ -109,26 +147,6 @@ class MessageLoader {
|
|
|
109
147
|
// Otherwise just assume it doesn't exist.
|
|
110
148
|
return undefined;
|
|
111
149
|
}
|
|
112
|
-
/**
|
|
113
|
-
* Returns a record mapping the keys this object manages to bound functions
|
|
114
|
-
* for `get` with the that key as the first argument, allowing consumers to
|
|
115
|
-
* just call the function with a locale to retrieve the translated message
|
|
116
|
-
* for that key.
|
|
117
|
-
*
|
|
118
|
-
* This method is provided as a way to generate binds _at runtime_, but for
|
|
119
|
-
* very-large messages files (e.g., thousands of messages), this can be a
|
|
120
|
-
* non-negligible cost. Where feasible, consider generating these binds
|
|
121
|
-
* at build/bundle time, especially in cases where they can be substantially
|
|
122
|
-
* minified (i.e., Hermes bytecode). This is provided automatically as an
|
|
123
|
-
* option when using one of `@discord/intl`'s transformers.
|
|
124
|
-
*/
|
|
125
|
-
getBinds() {
|
|
126
|
-
const result = {};
|
|
127
|
-
for (const key of this.messageKeys) {
|
|
128
|
-
result[key] = this.get.bind(this, key);
|
|
129
|
-
}
|
|
130
|
-
return result;
|
|
131
|
-
}
|
|
132
150
|
_loadLocale(locale) {
|
|
133
151
|
return __awaiter(this, void 0, void 0, function* () {
|
|
134
152
|
var _a, _b, _c, _d;
|
|
@@ -248,12 +266,15 @@ function waitForAllDefaultIntlMessagesLoaded() {
|
|
|
248
266
|
});
|
|
249
267
|
}
|
|
250
268
|
/**
|
|
251
|
-
* Create a new MessageLoader, which handles lazily loading messages for
|
|
252
|
-
*
|
|
253
|
-
*
|
|
269
|
+
* Create a new MessageLoader, which handles lazily loading messages for different locales and
|
|
270
|
+
* sanity checks as needed to provide accessors for each message contained by the import map.
|
|
271
|
+
*
|
|
272
|
+
* Notably, this does _not_ verify whether the locale objects managed by this loader actually
|
|
273
|
+
* contain a given key. It is the responsibility of the caller to verify this (easily enforced with
|
|
274
|
+
* typescript, eslint rules, and others).
|
|
254
275
|
*/
|
|
255
|
-
function createLoader(
|
|
256
|
-
const loader = new MessageLoader(
|
|
276
|
+
function createLoader(localeImportMap, defaultLocale) {
|
|
277
|
+
const loader = new MessageLoader(localeImportMap, defaultLocale);
|
|
257
278
|
LOADER_REGISTRY.push(loader);
|
|
258
279
|
return loader;
|
|
259
280
|
}
|
|
@@ -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.
|
|
3
|
+
"version": "0.19.1-canary.f6592a8",
|
|
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.
|
|
26
|
+
"@discord/intl-ast": "0.19.1-canary.f6592a8"
|
|
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.
|
package/src/message-loader.ts
CHANGED
|
@@ -58,10 +58,30 @@ export class MessageLoader {
|
|
|
58
58
|
_subscribers: Set<() => void>;
|
|
59
59
|
|
|
60
60
|
/**
|
|
61
|
-
*
|
|
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
|
///
|
|
@@ -85,8 +105,7 @@ export class MessageLoader {
|
|
|
85
105
|
*/
|
|
86
106
|
_localeFileMap?: Record<string, string>;
|
|
87
107
|
|
|
88
|
-
constructor(
|
|
89
|
-
this.messageKeys = messageKeys;
|
|
108
|
+
constructor(localeImportMap: LocaleImportMap, defaultLocale: LocaleId) {
|
|
90
109
|
this.messages = {};
|
|
91
110
|
this.localeImportMap = localeImportMap;
|
|
92
111
|
this.supportedLocales = Object.keys(localeImportMap);
|
|
@@ -126,6 +145,36 @@ export class MessageLoader {
|
|
|
126
145
|
this._localeFileMap = localeFileMap;
|
|
127
146
|
}
|
|
128
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
|
+
|
|
129
178
|
get(key: string, locale: LocaleId): InternalIntlMessage {
|
|
130
179
|
const expectedValue = this.getMessageValue(key, locale);
|
|
131
180
|
if (expectedValue != null) return expectedValue;
|
|
@@ -136,8 +185,13 @@ export class MessageLoader {
|
|
|
136
185
|
return this.fallbackMessage;
|
|
137
186
|
}
|
|
138
187
|
|
|
139
|
-
const
|
|
140
|
-
if (
|
|
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
|
+
}
|
|
141
195
|
|
|
142
196
|
// If the message couldn't be found in either the requested nor the default locale, then
|
|
143
197
|
// nothing can be done.
|
|
@@ -178,9 +232,16 @@ export class MessageLoader {
|
|
|
178
232
|
return undefined;
|
|
179
233
|
}
|
|
180
234
|
|
|
181
|
-
// Then try to return the loaded message.
|
|
182
|
-
|
|
183
|
-
|
|
235
|
+
// Then try to return the loaded message. Previously, this used a `key in ...` check to quickly
|
|
236
|
+
// verify whether the locale object contains the requested message, but in the case where the
|
|
237
|
+
// target is a Proxy or uses some other internalized method for providing gettable values, `in`
|
|
238
|
+
// queries aren't guaranteed to work. Instead, since this is realistically amortized to just be
|
|
239
|
+
// a property access on every call, we can just the returned value as the determining factor. If
|
|
240
|
+
// the locale object returns `undefined`, then it "does not contain" the message, whether it's
|
|
241
|
+
// still loading or otherwise. Future calls will then be able to pick up any changed value,
|
|
242
|
+
// since it won't be cached in the `_parseCache`.
|
|
243
|
+
const content = this.messages[locale][key];
|
|
244
|
+
if (content != null) {
|
|
184
245
|
const message = new InternalIntlMessage(content, locale);
|
|
185
246
|
(this._parseCache[locale] ??= {})[key] = message;
|
|
186
247
|
return message;
|
|
@@ -190,28 +251,6 @@ export class MessageLoader {
|
|
|
190
251
|
return undefined;
|
|
191
252
|
}
|
|
192
253
|
|
|
193
|
-
/**
|
|
194
|
-
* Returns a record mapping the keys this object manages to bound functions
|
|
195
|
-
* for `get` with the that key as the first argument, allowing consumers to
|
|
196
|
-
* just call the function with a locale to retrieve the translated message
|
|
197
|
-
* for that key.
|
|
198
|
-
*
|
|
199
|
-
* This method is provided as a way to generate binds _at runtime_, but for
|
|
200
|
-
* very-large messages files (e.g., thousands of messages), this can be a
|
|
201
|
-
* non-negligible cost. Where feasible, consider generating these binds
|
|
202
|
-
* at build/bundle time, especially in cases where they can be substantially
|
|
203
|
-
* minified (i.e., Hermes bytecode). This is provided automatically as an
|
|
204
|
-
* option when using one of `@discord/intl`'s transformers.
|
|
205
|
-
*/
|
|
206
|
-
getBinds(): Record<string, IntlMessageGetter> {
|
|
207
|
-
const result: Record<string, IntlMessageGetter> = {};
|
|
208
|
-
for (const key of this.messageKeys) {
|
|
209
|
-
result[key] = this.get.bind(this, key);
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
return result;
|
|
213
|
-
}
|
|
214
|
-
|
|
215
254
|
async _loadLocale(locale: LocaleId) {
|
|
216
255
|
// If the locale is already set in `messages`, then it doesn't need to be loaded again.
|
|
217
256
|
if (this.messages[locale] != null) return;
|
|
@@ -328,16 +367,15 @@ export async function waitForAllDefaultIntlMessagesLoaded(): Promise<void> {
|
|
|
328
367
|
}
|
|
329
368
|
|
|
330
369
|
/**
|
|
331
|
-
* Create a new MessageLoader, which handles lazily loading messages for
|
|
332
|
-
*
|
|
333
|
-
*
|
|
370
|
+
* Create a new MessageLoader, which handles lazily loading messages for different locales and
|
|
371
|
+
* sanity checks as needed to provide accessors for each message contained by the import map.
|
|
372
|
+
*
|
|
373
|
+
* Notably, this does _not_ verify whether the locale objects managed by this loader actually
|
|
374
|
+
* contain a given key. It is the responsibility of the caller to verify this (easily enforced with
|
|
375
|
+
* typescript, eslint rules, and others).
|
|
334
376
|
*/
|
|
335
|
-
export function createLoader(
|
|
336
|
-
|
|
337
|
-
localeImportMap: LocaleImportMap,
|
|
338
|
-
defaultLocale: LocaleId,
|
|
339
|
-
) {
|
|
340
|
-
const loader = new MessageLoader(messageKeys, localeImportMap, defaultLocale);
|
|
377
|
+
export function createLoader(localeImportMap: LocaleImportMap, defaultLocale: LocaleId) {
|
|
378
|
+
const loader = new MessageLoader(localeImportMap, defaultLocale);
|
|
341
379
|
LOADER_REGISTRY.push(loader);
|
|
342
380
|
return loader;
|
|
343
381
|
}
|
|
@@ -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
|
+
}
|