@discord/intl 0.2.0 → 0.2.1-canary.1ed7c43

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.
@@ -32,7 +32,18 @@ export class MessageLoader {
32
32
  * then a request has already been made to load the locale and it is waiting
33
33
  * to resolve.
34
34
  */
35
- _localeLoadingPromises: Record<LocaleId, Promise<{ default: MessagesData }> | undefined>;
35
+ _localeLoadingPromises: Record<
36
+ LocaleId,
37
+ {
38
+ /**`true` if this locale has been loaded at least once. */
39
+ initialized: boolean;
40
+ /**
41
+ * If the locale is currently being loaded, this Promise will exist and
42
+ * represent that loading status. Otherwise, this value is undefined.
43
+ */
44
+ current?: Promise<{ default: MessagesData }> | undefined;
45
+ }
46
+ >;
36
47
 
37
48
  /**
38
49
  * Map of pre-parsed messages, keyed by the message key and locale
@@ -85,7 +96,7 @@ export class MessageLoader {
85
96
  this._subscribers = new Set();
86
97
 
87
98
  this._loadLocale(this.defaultLocale);
88
- this.fallbackMessage = new InternalIntlMessage('THIS MESSAGE FAILED TO LOAD', 'en-US');
99
+ this.fallbackMessage = new InternalIntlMessage([], this.defaultLocale);
89
100
 
90
101
  // In cases where hot module replacement is available, set up cache clearing whenever the
91
102
  // targets change so that values are always replaced.
@@ -93,9 +104,9 @@ export class MessageLoader {
93
104
  if (module.hot) {
94
105
  for (const [locale, file] of Object.entries(localeImportMap)) {
95
106
  // @ts-expect-error `hot` not defined in types.
96
- module.hot.accept(file, () => {
107
+ module.hot.accept(file, async () => {
108
+ await this._loadLocale(locale);
97
109
  this._parseCache.clear();
98
- this._loadLocale(locale);
99
110
  });
100
111
  }
101
112
  }
@@ -116,9 +127,7 @@ export class MessageLoader {
116
127
  get(key: string, locale: LocaleId): InternalIntlMessage {
117
128
  const value =
118
129
  this.getMessageValue(key, locale) ?? this.getMessageValue(key, this.defaultLocale);
119
- if (value != null) {
120
- return value;
121
- }
130
+ if (value != null) return value;
122
131
 
123
132
  // If the message couldn't be found in either the requested nor the default locale, then
124
133
  // nothing can be done.
@@ -149,18 +158,14 @@ export class MessageLoader {
149
158
  const cachedValue = this._parseCache.get(cacheKey);
150
159
  if (cachedValue != null) return cachedValue;
151
160
 
152
- // Return early if this locale is still in the process of being loaded.
153
- if (this._localeLoadingPromises[locale] != null) {
154
- return undefined;
155
- }
156
-
157
- // If not, check whether it's been loaded and trigger a load if not.
161
+ // Check whether the locale exists in memory. If not, start a request to
162
+ // load the locale if one has not already started. It's impossible for
163
+ // the request to finish within the same tick, so return undefined in any
164
+ // case then.
158
165
  if (this.messages[locale] == null) {
159
- // Ensure the locale is loaded, if it is supported
160
166
  if (this.supportedLocales.includes(locale)) {
161
167
  this._loadLocale(locale);
162
168
  }
163
-
164
169
  return undefined;
165
170
  }
166
171
 
@@ -197,7 +202,7 @@ export class MessageLoader {
197
202
 
198
203
  async _loadLocale(locale: LocaleId) {
199
204
  // Don't re-load a locale that's already in progress.
200
- if (this._localeLoadingPromises[locale] != null) {
205
+ if (this._localeLoadingPromises[locale]?.current != null) {
201
206
  return;
202
207
  }
203
208
 
@@ -213,10 +218,10 @@ export class MessageLoader {
213
218
  // If the locale is already set in `messages`, then it doesn't need to be loaded again.
214
219
  if (this.messages[locale] != null) return;
215
220
 
216
- const loadingPromise = this.localeImportMap[locale]();
217
- this._localeLoadingPromises[locale] = loadingPromise;
218
- this.messages[locale] = (await loadingPromise).default;
219
- delete this._localeLoadingPromises[locale];
221
+ const current = this.localeImportMap[locale]();
222
+ this._localeLoadingPromises[locale] = { initialized: false, current };
223
+ this.messages[locale] = (await current).default;
224
+ this._localeLoadingPromises[locale] = { initialized: true, current: undefined };
220
225
  this.emitChange();
221
226
  }
222
227
 
@@ -238,4 +243,77 @@ export class MessageLoader {
238
243
 
239
244
  return () => this._subscribers.delete(callback);
240
245
  }
246
+
247
+ isLocaleLoaded(locale: LocaleId, requireCurrent: boolean = false): boolean {
248
+ const ref = this._localeLoadingPromises[locale];
249
+ // Ensure the locale at least exists and is initialized.
250
+ if (ref == null || ref.initialized == false) return false;
251
+ // `current` will be deleted once the locale has loaded, so if it is null,
252
+ // then the locale data is current.
253
+ if (requireCurrent) return ref.current == null;
254
+ // Otherwise, initialization is enough.
255
+ return true;
256
+ }
257
+
258
+ async waitForLocaleLoaded(locale: LocaleId, requireCurrent = false): Promise<void> {
259
+ const ref = this._localeLoadingPromises[locale];
260
+ // If the locale hasn't been loaded at all, kick off the load and return
261
+ // that Promise directly.
262
+ if (ref == null) return this._loadLocale(locale);
263
+ // If initialization is enough for this request, just resolve immediately.
264
+ if (ref.initialized && !requireCurrent) return;
265
+ // Otherwise, check the `current` loading state on the object, and if that
266
+ // doesn't exist, then the locale data is already finished loading, and the
267
+ // containing Promise will just resolve with `undefined`.
268
+ await ref.current;
269
+ }
270
+
271
+ /**
272
+ * Returns true if this loader's default locale has finished loading. When
273
+ * true, all messages managed by this loader are guaranteed to have _a_
274
+ * render-able value, even if one does not exist in the current locale.
275
+ */
276
+ async waitForDefaultLocale(requireCurrent = false): Promise<void> {
277
+ return this.waitForLocaleLoaded(this.defaultLocale, requireCurrent);
278
+ }
279
+ }
280
+
281
+ const LOADER_REGISTRY: MessageLoader[] = [];
282
+
283
+ /**
284
+ * Kick off a load request for the given locale across all currently-registered
285
+ * message loaders, returning a Promise that resolves when all loaders have
286
+ * finished loading that locale.
287
+ */
288
+ export async function loadAllMessagesInLocale(locale: LocaleId): Promise<void> {
289
+ await Promise.all(LOADER_REGISTRY.map((loader) => loader._loadLocale(locale)));
290
+ }
291
+
292
+ /**
293
+ * Returns a new Promise that resolves after all currently-registered message
294
+ * loaders have finished successfully loading their default locale content.
295
+ * Once this Promise has resolved, all messages that currently exist in the
296
+ * application (i.e., within all modules that have been imported or required in
297
+ * the current session) can be guaranteed to have _a_ render-able value.
298
+ *
299
+ * This function can be called at any point in an applications life cycle and
300
+ * will always ensure that _all_ loaders that currently exist are initialized.
301
+ */
302
+ export async function waitForAllDefaultIntlMessagesLoaded(): Promise<void> {
303
+ await Promise.all(LOADER_REGISTRY.map((loader) => loader.waitForDefaultLocale()));
304
+ }
305
+
306
+ /**
307
+ * Create a new MessageLoader, which handles lazily loading messages for
308
+ * different locales and sanity checks as needed to provide accessors for each
309
+ * message defined in `messageKeys`.
310
+ */
311
+ export function createLoader(
312
+ messageKeys: string[],
313
+ localeImportMap: LocaleImportMap,
314
+ defaultLocale: LocaleId,
315
+ ) {
316
+ const loader = new MessageLoader(messageKeys, localeImportMap, defaultLocale);
317
+ LOADER_REGISTRY.push(loader);
318
+ return loader;
241
319
  }
package/src/message.ts CHANGED
@@ -1,44 +1,129 @@
1
- import { FormatJsNode, FormatJsNodeType } from './keyless-json';
1
+ import { FormatJsNode, FormatJsNodeType, FormatJsTag } from './keyless-json';
2
2
 
3
3
  export class InternalIntlMessage {
4
4
  locale: string;
5
- message?: string;
6
5
  ast: FormatJsNode[];
7
6
 
7
+ constructor(messageOrAst: FormatJsNode[], locale: string) {
8
+ this.locale = locale;
9
+ this.ast = messageOrAst;
10
+ }
11
+
8
12
  /**
9
- * A stripped-down representation of the message with no rich formatting
10
- * elements preserved. This will be lazily created the first time it is
11
- * requested (e.g., by calling `formatToPlainString`).
13
+ * Return a stringified serialization of this message's AST, with no
14
+ * formatting or values applied.
12
15
  */
13
- get plainAst(): FormatJsNode[] {
14
- return (this._plainAst = this._removeRichTags(this.ast));
16
+ reserialize(): string {
17
+ const value = '';
18
+ serializeFormatJsAst(this.ast, { value });
19
+ return value;
15
20
  }
16
- _plainAst: FormatJsNode[];
17
-
18
- constructor(messageOrAst: string | FormatJsNode[], locale: string) {
19
- this.locale = locale;
21
+ }
20
22
 
21
- if (typeof messageOrAst === 'string') {
22
- this.message = messageOrAst;
23
- } else {
24
- this.ast = messageOrAst;
23
+ // Accepting an object as the `result` parameter lets the same string get passed around and
24
+ // appended, rather than creating a bunch of intermediate strings.
25
+ function serializeFormatJsAst(ast: FormatJsNode[], result: { value: string }) {
26
+ for (const node of ast) {
27
+ switch (node.type) {
28
+ case FormatJsNodeType.Literal:
29
+ result.value += node.value;
30
+ return;
31
+ case FormatJsNodeType.Argument:
32
+ // Empties are an artifact of our parsing strategy, not necessary here.
33
+ if (result.value === '$_') return;
34
+ result.value += '{' + node.value + '}';
35
+ return;
36
+ case FormatJsNodeType.Date:
37
+ result.value += '{' + node.value + ', date';
38
+ if (node.style != null) {
39
+ result.value += ', ' + node.style;
40
+ }
41
+ result.value += '}';
42
+ return;
43
+ case FormatJsNodeType.Time:
44
+ result.value += '{' + node.value + ', time';
45
+ if (node.style != null) {
46
+ result.value += ', ' + node.style;
47
+ }
48
+ result.value += '}';
49
+ return;
50
+ case FormatJsNodeType.Number:
51
+ result.value += '{' + node.value + ', number';
52
+ if (node.style != null) {
53
+ result.value += ', ' + node.style;
54
+ }
55
+ result.value += '}';
56
+ return;
57
+ case FormatJsNodeType.Plural: {
58
+ const pluralType = node.pluralType == 'ordinal' ? 'selectordinal' : 'plural';
59
+ result.value += '{' + node.value + ', ' + pluralType + ', ';
60
+ if (node.offset) {
61
+ result.value += 'offset:' + node.offset;
62
+ }
63
+ for (const [name, arm] of Object.entries(node.options)) {
64
+ result.value += ' ' + name + ' {';
65
+ serializeFormatJsAst(arm.value, result);
66
+ result.value += '}';
67
+ }
68
+ return;
69
+ }
70
+ case FormatJsNodeType.Pound:
71
+ result.value += '#';
72
+ return;
73
+ case FormatJsNodeType.Select: {
74
+ result.value += '{' + node.value + ', select, ';
75
+ for (const [name, arm] of Object.entries(node.options)) {
76
+ result.value += ' ' + name + ' {';
77
+ serializeFormatJsAst(arm.value, result);
78
+ result.value += '}';
79
+ }
80
+ return;
81
+ }
82
+ case FormatJsNodeType.Tag:
83
+ serializeFormatJsTag(node, result);
84
+ return;
25
85
  }
26
86
  }
87
+ }
27
88
 
28
- /**
29
- * Returns the same element with any formatting tags removed. If the element
30
- * itself is a tag, the children will be hoisted and returned, otherwise the
31
- * element is returned as an array of itself.
32
- */
33
- private _removeRichTags(nodes: FormatJsNode[]): FormatJsNode[] {
34
- const result = new Array(nodes.length);
35
- for (const node of nodes) {
36
- if (node.type === FormatJsNodeType.Tag) {
37
- result.push(...this._removeRichTags(node.children));
38
- } else {
39
- result.push(node);
89
+ function serializeFormatJsTag(node: FormatJsTag, result: { value: string }) {
90
+ switch (node.value) {
91
+ case '$b':
92
+ result.value += '**';
93
+ serializeFormatJsAst(node.children, result);
94
+ result.value += '**';
95
+ return;
96
+ case '$i':
97
+ result.value += '*';
98
+ serializeFormatJsAst(node.children, result);
99
+ result.value += '*';
100
+ return;
101
+ case '$code':
102
+ result.value += '`';
103
+ serializeFormatJsAst(node.children, result);
104
+ result.value += '`';
105
+ return;
106
+ case '$p':
107
+ serializeFormatJsAst(node.children, result);
108
+ result.value += '\n\n';
109
+ return;
110
+ case '$link':
111
+ // The target is the first child of the link. We don't have to care if it's a placeholder
112
+ // or not, because the serialization will automatically remove the extra empty.
113
+ const [target, ...children] = node.children;
114
+ result.value += '[';
115
+ serializeFormatJsAst(children, result);
116
+ result.value += '](';
117
+ if (target != null) {
118
+ serializeFormatJsAst([target], result);
40
119
  }
41
- }
42
- return result;
120
+ result.value += ')';
121
+ return;
122
+ default:
123
+ // Any other tag name is a hook, which just adds the `$[` on a link tag.
124
+ result.value += '$[';
125
+ serializeFormatJsAst(node.children, result);
126
+ result.value += '](' + node.value + ')';
127
+ return;
43
128
  }
44
129
  }
package/src/types.d.ts CHANGED
@@ -197,9 +197,9 @@ export interface FunctionTypeMap {
197
197
  handler: any;
198
198
  }
199
199
 
200
- export interface FunctionTypes<Result, HandlerType = (content: Result[]) => Result | Result[]> {
201
- link: undefined | ((content: Result[]) => Result | Result[]);
202
- hook: undefined | ((content: Result[]) => Result | Result[]);
200
+ export interface FunctionTypes<Result, HandlerType = () => void> {
201
+ link: undefined | ((content: Result[], key: string) => Result | Result[]);
202
+ hook: undefined | ((content: Result[], key: string) => Result | Result[]);
203
203
  handler: undefined | HandlerType;
204
204
  }
205
205
 
@@ -276,6 +276,7 @@ export interface RichTextFormattingMap<T = any> {
276
276
  $p: T;
277
277
  $link: T;
278
278
  $code: T;
279
+ $del: T;
279
280
  }
280
281
 
281
282
  export type RichTextTagNames = keyof RichTextFormattingMap;