@discord/intl 0.1.2 → 0.2.0-canary.641ab2f
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.d.ts +30 -0
- package/dist/format.js +214 -0
- package/dist/formatters/ast.d.ts +67 -0
- package/dist/formatters/ast.js +148 -0
- package/dist/formatters/index.d.ts +4 -0
- package/dist/formatters/index.js +14 -0
- package/dist/formatters/markdown.d.ts +13 -0
- package/dist/formatters/markdown.js +80 -0
- package/dist/formatters/react.d.ts +28 -0
- package/dist/formatters/react.js +111 -0
- package/dist/formatters/string.d.ts +21 -0
- package/dist/formatters/string.js +90 -0
- package/dist/index.d.ts +7 -6
- package/dist/index.js +19 -4
- package/dist/intl-manager.d.ts +25 -63
- package/dist/intl-manager.js +61 -89
- package/dist/keyless-json.d.ts +8 -1
- package/dist/message-loader.d.ts +38 -6
- package/dist/message-loader.js +117 -22
- package/dist/message.d.ts +3 -38
- package/dist/message.js +1 -132
- package/dist/types.d.ts +41 -45
- package/package.json +1 -2
- package/src/format.ts +224 -0
- package/src/formatters/ast.ts +155 -0
- package/src/formatters/index.ts +9 -0
- package/src/formatters/markdown.ts +54 -0
- package/src/formatters/react.ts +100 -0
- package/src/formatters/string.ts +62 -0
- package/src/index.ts +7 -7
- package/src/intl-manager.ts +73 -161
- package/src/keyless-json.ts +7 -0
- package/src/message-loader.ts +91 -24
- package/src/message.ts +4 -96
- package/src/types.d.ts +41 -45
- package/dist/react.d.ts +0 -38
- package/dist/react.js +0 -80
- package/src/react.ts +0 -80
package/src/intl-manager.ts
CHANGED
|
@@ -1,30 +1,16 @@
|
|
|
1
1
|
import { createIntl, IntlShape } from '@formatjs/intl';
|
|
2
|
-
import {
|
|
3
|
-
Formats,
|
|
4
|
-
Formatters,
|
|
5
|
-
IntlMessageFormat,
|
|
6
|
-
PART_TYPE as FormatPartType,
|
|
7
|
-
} from 'intl-messageformat';
|
|
2
|
+
import { Formats, Formatters, IntlMessageFormat } from 'intl-messageformat';
|
|
8
3
|
|
|
9
4
|
import { LocaleId, LocaleImportMap, MessageLoader } from './message-loader';
|
|
10
5
|
|
|
11
6
|
import type {
|
|
12
|
-
|
|
7
|
+
FormatterImplementation,
|
|
13
8
|
IntlMessageGetter,
|
|
14
9
|
RequiredFormatValues,
|
|
15
|
-
RichTextElementMap,
|
|
16
10
|
TypedIntlMessageGetter,
|
|
17
11
|
} from './types';
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
* Types for formatting functions when calling `formatToParts`, ensuring the
|
|
21
|
-
* functions yield value AST nodes.
|
|
22
|
-
*/
|
|
23
|
-
type AstFunctionTypes = {
|
|
24
|
-
link: undefined | object | ((children: object) => Array<string | any>);
|
|
25
|
-
hook: undefined | object | ((children: object) => Array<string | any>);
|
|
26
|
-
handler: undefined | object | ((children: object) => Array<string | any>);
|
|
27
|
-
};
|
|
12
|
+
import { InternalIntlMessage } from './message';
|
|
13
|
+
import { bindFormatValues, FormatBuilderConstructor } from './format';
|
|
28
14
|
|
|
29
15
|
/**
|
|
30
16
|
* Fallback locale used for all internationalization when an operation in the
|
|
@@ -32,26 +18,35 @@ type AstFunctionTypes = {
|
|
|
32
18
|
*/
|
|
33
19
|
export const DEFAULT_LOCALE: string = 'en-US';
|
|
34
20
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
21
|
+
type FormatterReturnType<F extends FormatterImplementation<any, any>> =
|
|
22
|
+
F extends FormatterImplementation<any, infer Return> ? Return : never;
|
|
23
|
+
|
|
24
|
+
type FormatterFunctionTypes<F extends FormatterImplementation<any, any>> =
|
|
25
|
+
F extends FormatterImplementation<infer Functions, any> ? Functions : never;
|
|
26
|
+
|
|
27
|
+
export type FormatFunction<F extends FormatterImplementation<any, any>> = <
|
|
28
|
+
T extends IntlMessageGetter,
|
|
29
|
+
>(
|
|
30
|
+
this: IntlManager,
|
|
31
|
+
message: T,
|
|
32
|
+
values: RequiredFormatValues<T, FormatterFunctionTypes<F>>,
|
|
33
|
+
) => FormatterReturnType<F>;
|
|
34
|
+
|
|
35
|
+
type ThisWithFormatters<
|
|
36
|
+
This,
|
|
37
|
+
T extends Record<string, FormatterImplementation<any, any>>,
|
|
38
|
+
> = This & {
|
|
39
|
+
[K in keyof T]: FormatFunction<T[K]>;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export class IntlManager {
|
|
39
43
|
defaultLocale: string;
|
|
40
44
|
currentLocale: string;
|
|
41
45
|
intl: IntlShape;
|
|
42
46
|
|
|
43
|
-
/**
|
|
44
|
-
* When formatting tag elements like `<b>value</b>`, these functions will
|
|
45
|
-
* automatically be used by all messages, unless they are explicitly removed
|
|
46
|
-
* for a specific call, either by overriding the value or by calling a
|
|
47
|
-
* dedicated method like `formatToPlainString` which will remove all
|
|
48
|
-
* rich elements from the string and return only the plain text result.
|
|
49
|
-
*/
|
|
50
|
-
defaultRichTextElements: DefaultElements;
|
|
51
|
-
|
|
52
47
|
_localeSubscriptions: Set<(locale: string) => void>;
|
|
53
48
|
|
|
54
|
-
constructor(defaultLocale: string = DEFAULT_LOCALE
|
|
49
|
+
constructor(defaultLocale: string = DEFAULT_LOCALE) {
|
|
55
50
|
this.defaultLocale = defaultLocale;
|
|
56
51
|
this.currentLocale = defaultLocale;
|
|
57
52
|
this.intl = createIntl({
|
|
@@ -60,10 +55,38 @@ export class IntlManager<
|
|
|
60
55
|
locale: defaultLocale,
|
|
61
56
|
});
|
|
62
57
|
|
|
63
|
-
this.defaultRichTextElements = defaultRichTextElements;
|
|
64
58
|
this._localeSubscriptions = new Set();
|
|
65
59
|
}
|
|
66
60
|
|
|
61
|
+
/**
|
|
62
|
+
* Add a set of formatter implementations to this manager, making each available as a direct
|
|
63
|
+
* property
|
|
64
|
+
*/
|
|
65
|
+
withFormatters<const T extends Record<string, FormatterImplementation<any, any>>>(
|
|
66
|
+
formatters: T,
|
|
67
|
+
): ThisWithFormatters<this, T> {
|
|
68
|
+
for (const [name, formatter] of Object.entries(formatters)) {
|
|
69
|
+
this[name] = this.makeFormatFunction(formatter);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return this as ThisWithFormatters<this, T>;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Return a new function bound to this manager that uses the given `FormatterImplementation` to
|
|
77
|
+
* format a message after it has been resolved for the current locale and potentially processed in
|
|
78
|
+
* other ways by the manager.
|
|
79
|
+
*/
|
|
80
|
+
makeFormatFunction<F extends FormatterImplementation<any, any>>({
|
|
81
|
+
format,
|
|
82
|
+
builder,
|
|
83
|
+
}: F): FormatFunction<FormatterReturnType<F>> {
|
|
84
|
+
const formatter = format.bind(this);
|
|
85
|
+
return (message, values) => {
|
|
86
|
+
return formatter(message(this.currentLocale), values, builder);
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
67
90
|
/**
|
|
68
91
|
* Set the locale for this package to use. This is a global change, and all
|
|
69
92
|
* methods on this object will use this locale for formatting.
|
|
@@ -89,140 +112,29 @@ export class IntlManager<
|
|
|
89
112
|
};
|
|
90
113
|
|
|
91
114
|
/**
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
* a simple string value.
|
|
95
|
-
*
|
|
96
|
-
* This function is the basis of how messages are normally formatted, and can
|
|
97
|
-
* be used anywhere. However, it is not reactive and only functions on the
|
|
98
|
-
* data that is currently loaded and known. For a reactive function that
|
|
99
|
-
* automatically updates when the locale changes or when new data is loaded,
|
|
100
|
-
* use `format`, which will wrap the formatting in a React component that
|
|
101
|
-
* subscribes to the current locale and state of loaded messages.
|
|
115
|
+
* For static messages with no rich text and no dynamic placeholders, use this method to
|
|
116
|
+
* immediately return the plain string value of the message in the current locale.
|
|
102
117
|
*/
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
message: T,
|
|
110
|
-
values?: RequiredFormatValues<T, DefaultElements, AstFunctionTypes>,
|
|
111
|
-
): Array<string | any> {
|
|
112
|
-
if (typeof message === 'string') return [message];
|
|
113
|
-
const resolvedMessage = typeof message === 'function' ? message(this.currentLocale) : message;
|
|
114
|
-
if (typeof resolvedMessage === 'string') return [resolvedMessage];
|
|
115
|
-
|
|
116
|
-
const resolvedValues =
|
|
117
|
-
values != null
|
|
118
|
-
? { ...this.defaultRichTextElements, ...values }
|
|
119
|
-
: this.defaultRichTextElements;
|
|
120
|
-
const parts = resolvedMessage.formatToParts(
|
|
121
|
-
this.intl.formatters as Formatters,
|
|
122
|
-
this.intl.formats as Formats,
|
|
123
|
-
resolvedValues,
|
|
124
|
-
);
|
|
125
|
-
|
|
126
|
-
const result = [];
|
|
127
|
-
let inLiteral = false;
|
|
128
|
-
for (const part of parts) {
|
|
129
|
-
// This condition merges consecutive literal elements (static strings)
|
|
130
|
-
// into single parts to reduce the number of nodes in the result. This
|
|
131
|
-
// condition will never be true on the first loop, ensuring that the
|
|
132
|
-
// array must at least have one entry before it attempts to concatenate
|
|
133
|
-
// onto that value again.
|
|
134
|
-
if (inLiteral && (inLiteral = part.type === FormatPartType.literal)) {
|
|
135
|
-
result[result.length - 1] += part.value;
|
|
136
|
-
continue;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
inLiteral = part.type === FormatPartType.literal;
|
|
140
|
-
result.push(part.value);
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
return result;
|
|
118
|
+
string<T extends TypedIntlMessageGetter<undefined>>(message: T): string {
|
|
119
|
+
// @ts-expect-error Figure out how to make this typing exact. This
|
|
120
|
+
// currently relies on the generic typing being sound enough to know that
|
|
121
|
+
// the message can only contain a single static text node and no
|
|
122
|
+
// placeholders or rich text.
|
|
123
|
+
return message(this.currentLocale).ast[0].value;
|
|
144
124
|
}
|
|
145
125
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
string(message: TypedIntlMessageGetter<undefined>): string {
|
|
156
|
-
// TODO(faulty): Optimize this so that plain strings are hotpathed even faster.
|
|
157
|
-
return this.formatToPlainString(message);
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
/**
|
|
161
|
-
* Format the given message with the provided values, removing any styling
|
|
162
|
-
* and non-textual content from the message, returning a plain string.
|
|
163
|
-
*/
|
|
164
|
-
formatToPlainString<T extends TypedIntlMessageGetter<undefined>>(message: T): string;
|
|
165
|
-
formatToPlainString<T extends IntlMessageGetter>(
|
|
166
|
-
message: T,
|
|
167
|
-
values: RequiredFormatValues<T, DefaultElements, FunctionTypes>,
|
|
168
|
-
): string;
|
|
169
|
-
formatToPlainString<T extends IntlMessageGetter>(
|
|
170
|
-
message: T,
|
|
171
|
-
values?: RequiredFormatValues<T, DefaultElements, FunctionTypes>,
|
|
172
|
-
): string {
|
|
173
|
-
if (typeof message === 'string') return message;
|
|
174
|
-
const resolvedMessage = message(this.currentLocale);
|
|
175
|
-
if (typeof resolvedMessage === 'string') return resolvedMessage;
|
|
176
|
-
|
|
177
|
-
// No need to pass in `defaultRichTextElements`, since the stylistic tags
|
|
178
|
-
// will be removed from the string anyway.
|
|
179
|
-
return resolvedMessage.formatToPlainString(
|
|
126
|
+
bindFormatValues<T>(
|
|
127
|
+
Builder: FormatBuilderConstructor<T>,
|
|
128
|
+
message: InternalIntlMessage,
|
|
129
|
+
values: Record<string, any>,
|
|
130
|
+
): T[] {
|
|
131
|
+
return bindFormatValues(
|
|
132
|
+
Builder,
|
|
133
|
+
message.ast,
|
|
134
|
+
[this.currentLocale, this.defaultLocale],
|
|
180
135
|
this.intl.formatters as Formatters,
|
|
181
136
|
this.intl.formats as Formats,
|
|
182
137
|
values,
|
|
183
138
|
);
|
|
184
139
|
}
|
|
185
|
-
|
|
186
|
-
/**
|
|
187
|
-
* Similar to `formatToPlainString`, format the given message with the provided
|
|
188
|
-
* values, but convert all rich text formatting back to Markdown syntax rather
|
|
189
|
-
* than rendering the actual rich content. The result is a plain string that
|
|
190
|
-
* can be sent through a separate Markdown renderer to get an equivalent
|
|
191
|
-
* result to formatting this message directly.
|
|
192
|
-
*/
|
|
193
|
-
formatToMarkdownString<T extends TypedIntlMessageGetter<undefined>>(message: T): string;
|
|
194
|
-
formatToMarkdownString<T extends IntlMessageGetter>(
|
|
195
|
-
message: T,
|
|
196
|
-
values: RequiredFormatValues<T, DefaultElements, FunctionTypes>,
|
|
197
|
-
): string;
|
|
198
|
-
formatToMarkdownString<T extends IntlMessageGetter>(
|
|
199
|
-
message: T,
|
|
200
|
-
values?: RequiredFormatValues<T, DefaultElements, FunctionTypes>,
|
|
201
|
-
): string {
|
|
202
|
-
// TODO(faulty): Implement the markdown syntax conversion here.
|
|
203
|
-
if (typeof message === 'string') return message;
|
|
204
|
-
const resolvedMessage = message(this.currentLocale);
|
|
205
|
-
if (typeof resolvedMessage === 'string') return resolvedMessage;
|
|
206
|
-
|
|
207
|
-
// No need to pass in `defaultRichTextElements`, since the stylistic tags
|
|
208
|
-
// will be removed from the string anyway.
|
|
209
|
-
return resolvedMessage.formatToPlainString(
|
|
210
|
-
this.intl.formatters as Formatters,
|
|
211
|
-
this.intl.formats as Formats,
|
|
212
|
-
values,
|
|
213
|
-
);
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
/**
|
|
218
|
-
* Create a new MessageLoader, which handles lazily loading messages for
|
|
219
|
-
* different locales and sanity checks as needed to provide accessors for each
|
|
220
|
-
* message defined in `messageKeys`.
|
|
221
|
-
*/
|
|
222
|
-
export function createLoader(
|
|
223
|
-
messageKeys: string[],
|
|
224
|
-
localeImportMap: LocaleImportMap,
|
|
225
|
-
defaultLocale: LocaleId,
|
|
226
|
-
) {
|
|
227
|
-
return new MessageLoader(messageKeys, localeImportMap, defaultLocale);
|
|
228
140
|
}
|
package/src/keyless-json.ts
CHANGED
|
@@ -38,6 +38,12 @@ export interface FormatJsTime {
|
|
|
38
38
|
style?: string;
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
export interface FormatJsSelect {
|
|
42
|
+
type: FormatJsNodeType.Select;
|
|
43
|
+
value: string;
|
|
44
|
+
options: Record<string, { value: FormatJsNode[] }>;
|
|
45
|
+
}
|
|
46
|
+
|
|
41
47
|
export type FormatJsPluralType = 'cardinal' | 'ordinal';
|
|
42
48
|
|
|
43
49
|
export interface FormatJsPlural {
|
|
@@ -64,6 +70,7 @@ export type FormatJsNode =
|
|
|
64
70
|
| FormatJsNumber
|
|
65
71
|
| FormatJsDate
|
|
66
72
|
| FormatJsTime
|
|
73
|
+
| FormatJsSelect
|
|
67
74
|
| FormatJsPlural
|
|
68
75
|
| FormatJsPound
|
|
69
76
|
| FormatJsTag;
|
package/src/message-loader.ts
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
|
+
import { FormatJsNode } from './keyless-json';
|
|
1
2
|
import { InternalIntlMessage } from './message';
|
|
2
3
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
type AnyRawMessage = string | MessageFormatElement[];
|
|
6
|
-
type MessagesData = Record<string, AnyRawMessage>;
|
|
4
|
+
type MessagesData = Record<string, FormatJsNode[]>;
|
|
7
5
|
export interface IntlMessageGetterAdditions {
|
|
8
6
|
onChange(callback: () => void): () => void;
|
|
9
7
|
}
|
|
@@ -34,7 +32,18 @@ export class MessageLoader {
|
|
|
34
32
|
* then a request has already been made to load the locale and it is waiting
|
|
35
33
|
* to resolve.
|
|
36
34
|
*/
|
|
37
|
-
_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
|
+
>;
|
|
38
47
|
|
|
39
48
|
/**
|
|
40
49
|
* Map of pre-parsed messages, keyed by the message key and locale
|
|
@@ -87,7 +96,7 @@ export class MessageLoader {
|
|
|
87
96
|
this._subscribers = new Set();
|
|
88
97
|
|
|
89
98
|
this._loadLocale(this.defaultLocale);
|
|
90
|
-
this.fallbackMessage = new InternalIntlMessage(
|
|
99
|
+
this.fallbackMessage = new InternalIntlMessage([], this.defaultLocale);
|
|
91
100
|
|
|
92
101
|
// In cases where hot module replacement is available, set up cache clearing whenever the
|
|
93
102
|
// targets change so that values are always replaced.
|
|
@@ -95,9 +104,9 @@ export class MessageLoader {
|
|
|
95
104
|
if (module.hot) {
|
|
96
105
|
for (const [locale, file] of Object.entries(localeImportMap)) {
|
|
97
106
|
// @ts-expect-error `hot` not defined in types.
|
|
98
|
-
module.hot.accept(file, () => {
|
|
107
|
+
module.hot.accept(file, async () => {
|
|
108
|
+
await this._loadLocale(locale);
|
|
99
109
|
this._parseCache.clear();
|
|
100
|
-
this._loadLocale(locale);
|
|
101
110
|
});
|
|
102
111
|
}
|
|
103
112
|
}
|
|
@@ -118,9 +127,7 @@ export class MessageLoader {
|
|
|
118
127
|
get(key: string, locale: LocaleId): InternalIntlMessage {
|
|
119
128
|
const value =
|
|
120
129
|
this.getMessageValue(key, locale) ?? this.getMessageValue(key, this.defaultLocale);
|
|
121
|
-
if (value != null)
|
|
122
|
-
return value;
|
|
123
|
-
}
|
|
130
|
+
if (value != null) return value;
|
|
124
131
|
|
|
125
132
|
// If the message couldn't be found in either the requested nor the default locale, then
|
|
126
133
|
// nothing can be done.
|
|
@@ -151,18 +158,14 @@ export class MessageLoader {
|
|
|
151
158
|
const cachedValue = this._parseCache.get(cacheKey);
|
|
152
159
|
if (cachedValue != null) return cachedValue;
|
|
153
160
|
|
|
154
|
-
//
|
|
155
|
-
if
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
// 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.
|
|
160
165
|
if (this.messages[locale] == null) {
|
|
161
|
-
// Ensure the locale is loaded, if it is supported
|
|
162
166
|
if (this.supportedLocales.includes(locale)) {
|
|
163
167
|
this._loadLocale(locale);
|
|
164
168
|
}
|
|
165
|
-
|
|
166
169
|
return undefined;
|
|
167
170
|
}
|
|
168
171
|
|
|
@@ -199,7 +202,7 @@ export class MessageLoader {
|
|
|
199
202
|
|
|
200
203
|
async _loadLocale(locale: LocaleId) {
|
|
201
204
|
// Don't re-load a locale that's already in progress.
|
|
202
|
-
if (this._localeLoadingPromises[locale] != null) {
|
|
205
|
+
if (this._localeLoadingPromises[locale]?.current != null) {
|
|
203
206
|
return;
|
|
204
207
|
}
|
|
205
208
|
|
|
@@ -215,10 +218,10 @@ export class MessageLoader {
|
|
|
215
218
|
// If the locale is already set in `messages`, then it doesn't need to be loaded again.
|
|
216
219
|
if (this.messages[locale] != null) return;
|
|
217
220
|
|
|
218
|
-
const
|
|
219
|
-
this._localeLoadingPromises[locale] =
|
|
220
|
-
this.messages[locale] = (await
|
|
221
|
-
|
|
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 };
|
|
222
225
|
this.emitChange();
|
|
223
226
|
}
|
|
224
227
|
|
|
@@ -240,4 +243,68 @@ export class MessageLoader {
|
|
|
240
243
|
|
|
241
244
|
return () => this._subscribers.delete(callback);
|
|
242
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
|
+
* Returns a new Promise that resolves after all currently-registered message
|
|
285
|
+
* loaders have finished successfully loading their default locale content.
|
|
286
|
+
* Once this Promise has resolved, all messages that currently exist in the
|
|
287
|
+
* application (i.e., within all modules that have been imported or required in
|
|
288
|
+
* the current session) can be guaranteed to have _a_ render-able value.
|
|
289
|
+
*
|
|
290
|
+
* This function can be called at any point in an applications life cycle and
|
|
291
|
+
* will always ensure that _all_ loaders that currently exist are initialized.
|
|
292
|
+
*/
|
|
293
|
+
export async function waitForAllDefaultIntlMessagesLoaded(): Promise<void> {
|
|
294
|
+
await Promise.all(LOADER_REGISTRY.map((loader) => loader.waitForDefaultLocale()));
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Create a new MessageLoader, which handles lazily loading messages for
|
|
299
|
+
* different locales and sanity checks as needed to provide accessors for each
|
|
300
|
+
* message defined in `messageKeys`.
|
|
301
|
+
*/
|
|
302
|
+
export function createLoader(
|
|
303
|
+
messageKeys: string[],
|
|
304
|
+
localeImportMap: LocaleImportMap,
|
|
305
|
+
defaultLocale: LocaleId,
|
|
306
|
+
) {
|
|
307
|
+
const loader = new MessageLoader(messageKeys, localeImportMap, defaultLocale);
|
|
308
|
+
LOADER_REGISTRY.push(loader);
|
|
309
|
+
return loader;
|
|
243
310
|
}
|
package/src/message.ts
CHANGED
|
@@ -1,103 +1,11 @@
|
|
|
1
|
-
import {
|
|
2
|
-
TYPE as FormatElementType,
|
|
3
|
-
parse as parseMessageFormat,
|
|
4
|
-
} from '@formatjs/icu-messageformat-parser';
|
|
5
|
-
import {
|
|
6
|
-
Formats,
|
|
7
|
-
Formatters,
|
|
8
|
-
MessageFormatPart,
|
|
9
|
-
formatToParts as libFormatToParts,
|
|
10
|
-
} from 'intl-messageformat';
|
|
11
|
-
|
|
12
|
-
import type { MessageFormatElement } from '@formatjs/icu-messageformat-parser';
|
|
1
|
+
import { FormatJsNode } from './keyless-json';
|
|
13
2
|
|
|
14
3
|
export class InternalIntlMessage {
|
|
15
4
|
locale: string;
|
|
16
|
-
|
|
17
|
-
ast: MessageFormatElement[];
|
|
18
|
-
/**
|
|
19
|
-
* A stripped-down representation of the message with no rich formatting
|
|
20
|
-
* elements preserved. This will be lazily created the first time it is
|
|
21
|
-
* requested (e.g., by calling `formatToPlainString`).
|
|
22
|
-
*/
|
|
23
|
-
plainAst?: MessageFormatElement[];
|
|
5
|
+
ast: FormatJsNode[];
|
|
24
6
|
|
|
25
|
-
constructor(messageOrAst:
|
|
7
|
+
constructor(messageOrAst: FormatJsNode[], locale: string) {
|
|
26
8
|
this.locale = locale;
|
|
27
|
-
|
|
28
|
-
if (typeof messageOrAst === 'string') {
|
|
29
|
-
this.message = messageOrAst;
|
|
30
|
-
this.ast = parseMessageFormat(messageOrAst);
|
|
31
|
-
} else {
|
|
32
|
-
this.ast = messageOrAst;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
this.plainAst = undefined;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Format this message with the given locale into an AST of chunks that can
|
|
40
|
-
* then be used or rendered into strings, React elements, or anything else as
|
|
41
|
-
* needed.
|
|
42
|
-
*/
|
|
43
|
-
formatToParts<T>(
|
|
44
|
-
formatters: Formatters,
|
|
45
|
-
formats: Formats,
|
|
46
|
-
values?: Record<string, any>,
|
|
47
|
-
): Array<MessageFormatPart<T>> {
|
|
48
|
-
return libFormatToParts(this.ast, this.locale, formatters, formats, values);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* Formats this message just like `formatToParts`, but strips all stylistic
|
|
53
|
-
* elements and tags and returns a plain string with only the literal text of
|
|
54
|
-
* the message remaining.
|
|
55
|
-
*
|
|
56
|
-
* This method works by quickly scanning the string and removing any tag
|
|
57
|
-
* elements, formatting, then concatenating the pieces into a single result.
|
|
58
|
-
*
|
|
59
|
-
* Note that this function does _not_ handle serializing values other than
|
|
60
|
-
* plain/primitive values into strings. All values are simply converted using
|
|
61
|
-
* the `String()` constructor. So, for example, passing a function as a value
|
|
62
|
-
* will directly serialize the function definition into a string, rather than
|
|
63
|
-
* calling the function or evaluating the result.
|
|
64
|
-
*/
|
|
65
|
-
formatToPlainString(
|
|
66
|
-
formatters: Formatters,
|
|
67
|
-
formats: Formats,
|
|
68
|
-
values?: Record<string, any>,
|
|
69
|
-
): string {
|
|
70
|
-
// Lazily create the reduced AST if it hasn't already been done.
|
|
71
|
-
if (this.plainAst == null) {
|
|
72
|
-
this.plainAst = [];
|
|
73
|
-
for (const part of this.ast) {
|
|
74
|
-
this.plainAst.push(...this._removeRichTags(part));
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
const parts = libFormatToParts(this.plainAst, this.locale, formatters, formats, values);
|
|
79
|
-
let result = '';
|
|
80
|
-
for (const part of parts) {
|
|
81
|
-
result += String(part.value);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
return result;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* Returns the same element with any formatting tags removed. If the element
|
|
89
|
-
* itself is a tag, the children will be hoisted and returned, otherwise the
|
|
90
|
-
* element is returned as an array of itself.
|
|
91
|
-
*/
|
|
92
|
-
_removeRichTags(element: MessageFormatElement): MessageFormatElement[] {
|
|
93
|
-
if (element.type === FormatElementType.tag) {
|
|
94
|
-
const childContent = [];
|
|
95
|
-
for (const child of element.children) {
|
|
96
|
-
childContent.push(...this._removeRichTags(child));
|
|
97
|
-
}
|
|
98
|
-
return childContent;
|
|
99
|
-
} else {
|
|
100
|
-
return [element];
|
|
101
|
-
}
|
|
9
|
+
this.ast = messageOrAst;
|
|
102
10
|
}
|
|
103
11
|
}
|