@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.
- package/dist/format.js +17 -14
- package/dist/formatters/ast.d.ts +6 -2
- package/dist/formatters/ast.js +4 -10
- package/dist/formatters/markdown.js +3 -2
- package/dist/formatters/react.d.ts +6 -3
- package/dist/formatters/react.js +9 -7
- package/dist/index.d.ts +2 -1
- package/dist/index.js +7 -3
- package/dist/intl-manager.d.ts +11 -8
- package/dist/intl-manager.js +19 -12
- package/dist/message-loader.d.ts +42 -3
- package/dist/message-loader.js +135 -22
- package/dist/message.d.ts +4 -13
- package/dist/message.js +144 -51
- package/dist/types.d.ts +4 -3
- package/package.json +2 -1
- package/src/format.ts +24 -15
- package/src/formatters/ast.ts +9 -12
- package/src/formatters/markdown.ts +3 -2
- package/src/formatters/react.ts +17 -10
- package/src/index.ts +8 -2
- package/src/intl-manager.ts +22 -19
- package/src/message-loader.ts +98 -20
- package/src/message.ts +114 -29
- package/src/types.d.ts +4 -3
package/src/message-loader.ts
CHANGED
|
@@ -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<
|
|
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(
|
|
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
|
-
//
|
|
153
|
-
if
|
|
154
|
-
|
|
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
|
|
217
|
-
this._localeLoadingPromises[locale] =
|
|
218
|
-
this.messages[locale] = (await
|
|
219
|
-
|
|
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
|
-
*
|
|
10
|
-
*
|
|
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
|
-
|
|
14
|
-
|
|
16
|
+
reserialize(): string {
|
|
17
|
+
const value = '';
|
|
18
|
+
serializeFormatJsAst(this.ast, { value });
|
|
19
|
+
return value;
|
|
15
20
|
}
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
constructor(messageOrAst: string | FormatJsNode[], locale: string) {
|
|
19
|
-
this.locale = locale;
|
|
21
|
+
}
|
|
20
22
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
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 = (
|
|
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;
|