@discord/intl 0.1.3-rc.0 → 0.2.0
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 +6 -6
- package/dist/index.js +16 -3
- package/dist/intl-manager.d.ts +25 -56
- package/dist/intl-manager.js +58 -79
- package/dist/keyless-json.d.ts +8 -1
- package/dist/message-loader.d.ts +2 -3
- package/dist/message.d.ts +6 -27
- package/dist/message.js +29 -78
- 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 +6 -6
- package/src/intl-manager.ts +70 -148
- package/src/keyless-json.ts +7 -0
- package/src/message-loader.ts +2 -4
- package/src/message.ts +16 -75
- 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/dist/message.d.ts
CHANGED
|
@@ -1,41 +1,20 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import type { MessageFormatElement } from '@formatjs/icu-messageformat-parser';
|
|
1
|
+
import { FormatJsNode } from './keyless-json';
|
|
3
2
|
export declare class InternalIntlMessage {
|
|
4
3
|
locale: string;
|
|
5
4
|
message?: string;
|
|
6
|
-
ast:
|
|
5
|
+
ast: FormatJsNode[];
|
|
7
6
|
/**
|
|
8
7
|
* A stripped-down representation of the message with no rich formatting
|
|
9
8
|
* elements preserved. This will be lazily created the first time it is
|
|
10
9
|
* requested (e.g., by calling `formatToPlainString`).
|
|
11
10
|
*/
|
|
12
|
-
plainAst
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
* Format this message with the given locale into an AST of chunks that can
|
|
16
|
-
* then be used or rendered into strings, React elements, or anything else as
|
|
17
|
-
* needed.
|
|
18
|
-
*/
|
|
19
|
-
formatToParts<T>(formatters: Formatters, formats: Formats, values?: Record<string, any>): Array<MessageFormatPart<T>>;
|
|
20
|
-
/**
|
|
21
|
-
* Formats this message just like `formatToParts`, but strips all stylistic
|
|
22
|
-
* elements and tags and returns a plain string with only the literal text of
|
|
23
|
-
* the message remaining.
|
|
24
|
-
*
|
|
25
|
-
* This method works by quickly scanning the string and removing any tag
|
|
26
|
-
* elements, formatting, then concatenating the pieces into a single result.
|
|
27
|
-
*
|
|
28
|
-
* Note that this function does _not_ handle serializing values other than
|
|
29
|
-
* plain/primitive values into strings. All values are simply converted using
|
|
30
|
-
* the `String()` constructor. So, for example, passing a function as a value
|
|
31
|
-
* will directly serialize the function definition into a string, rather than
|
|
32
|
-
* calling the function or evaluating the result.
|
|
33
|
-
*/
|
|
34
|
-
formatToPlainString(formatters: Formatters, formats: Formats, values?: Record<string, any>): string;
|
|
11
|
+
get plainAst(): FormatJsNode[];
|
|
12
|
+
_plainAst: FormatJsNode[];
|
|
13
|
+
constructor(messageOrAst: string | FormatJsNode[], locale: string);
|
|
35
14
|
/**
|
|
36
15
|
* Returns the same element with any formatting tags removed. If the element
|
|
37
16
|
* itself is a tag, the children will be hoisted and returned, otherwise the
|
|
38
17
|
* element is returned as an array of itself.
|
|
39
18
|
*/
|
|
40
|
-
_removeRichTags
|
|
19
|
+
private _removeRichTags;
|
|
41
20
|
}
|
package/dist/message.js
CHANGED
|
@@ -37,106 +37,57 @@ var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) {
|
|
|
37
37
|
};
|
|
38
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
39
|
exports.InternalIntlMessage = void 0;
|
|
40
|
-
var
|
|
41
|
-
var intl_messageformat_1 = require("intl-messageformat");
|
|
40
|
+
var keyless_json_1 = require("./keyless-json");
|
|
42
41
|
var InternalIntlMessage = /** @class */ (function () {
|
|
43
42
|
function InternalIntlMessage(messageOrAst, locale) {
|
|
44
43
|
this.locale = locale;
|
|
45
44
|
if (typeof messageOrAst === 'string') {
|
|
46
45
|
this.message = messageOrAst;
|
|
47
|
-
this.ast = (0, icu_messageformat_parser_1.parse)(messageOrAst);
|
|
48
46
|
}
|
|
49
47
|
else {
|
|
50
48
|
this.ast = messageOrAst;
|
|
51
49
|
}
|
|
52
|
-
this.plainAst = undefined;
|
|
53
50
|
}
|
|
51
|
+
Object.defineProperty(InternalIntlMessage.prototype, "plainAst", {
|
|
52
|
+
/**
|
|
53
|
+
* A stripped-down representation of the message with no rich formatting
|
|
54
|
+
* elements preserved. This will be lazily created the first time it is
|
|
55
|
+
* requested (e.g., by calling `formatToPlainString`).
|
|
56
|
+
*/
|
|
57
|
+
get: function () {
|
|
58
|
+
return (this._plainAst = this._removeRichTags(this.ast));
|
|
59
|
+
},
|
|
60
|
+
enumerable: false,
|
|
61
|
+
configurable: true
|
|
62
|
+
});
|
|
54
63
|
/**
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*/
|
|
59
|
-
InternalIntlMessage.prototype.formatToParts = function (formatters, formats, values) {
|
|
60
|
-
return (0, intl_messageformat_1.formatToParts)(this.ast, this.locale, formatters, formats, values);
|
|
61
|
-
};
|
|
62
|
-
/**
|
|
63
|
-
* Formats this message just like `formatToParts`, but strips all stylistic
|
|
64
|
-
* elements and tags and returns a plain string with only the literal text of
|
|
65
|
-
* the message remaining.
|
|
66
|
-
*
|
|
67
|
-
* This method works by quickly scanning the string and removing any tag
|
|
68
|
-
* elements, formatting, then concatenating the pieces into a single result.
|
|
69
|
-
*
|
|
70
|
-
* Note that this function does _not_ handle serializing values other than
|
|
71
|
-
* plain/primitive values into strings. All values are simply converted using
|
|
72
|
-
* the `String()` constructor. So, for example, passing a function as a value
|
|
73
|
-
* will directly serialize the function definition into a string, rather than
|
|
74
|
-
* calling the function or evaluating the result.
|
|
64
|
+
* Returns the same element with any formatting tags removed. If the element
|
|
65
|
+
* itself is a tag, the children will be hoisted and returned, otherwise the
|
|
66
|
+
* element is returned as an array of itself.
|
|
75
67
|
*/
|
|
76
|
-
InternalIntlMessage.prototype.
|
|
77
|
-
var e_1, _a
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
(_b = this.plainAst).push.apply(_b, __spreadArray([], __read(this._removeRichTags(part)), false));
|
|
68
|
+
InternalIntlMessage.prototype._removeRichTags = function (nodes) {
|
|
69
|
+
var e_1, _a;
|
|
70
|
+
var result = new Array(nodes.length);
|
|
71
|
+
try {
|
|
72
|
+
for (var nodes_1 = __values(nodes), nodes_1_1 = nodes_1.next(); !nodes_1_1.done; nodes_1_1 = nodes_1.next()) {
|
|
73
|
+
var node = nodes_1_1.value;
|
|
74
|
+
if (node.type === keyless_json_1.FormatJsNodeType.Tag) {
|
|
75
|
+
result.push.apply(result, __spreadArray([], __read(this._removeRichTags(node.children)), false));
|
|
85
76
|
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
finally {
|
|
89
|
-
try {
|
|
90
|
-
if (_e && !_e.done && (_a = _d.return)) _a.call(_d);
|
|
77
|
+
else {
|
|
78
|
+
result.push(node);
|
|
91
79
|
}
|
|
92
|
-
finally { if (e_1) throw e_1.error; }
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
var parts = (0, intl_messageformat_1.formatToParts)(this.plainAst, this.locale, formatters, formats, values);
|
|
96
|
-
var result = '';
|
|
97
|
-
try {
|
|
98
|
-
for (var parts_1 = __values(parts), parts_1_1 = parts_1.next(); !parts_1_1.done; parts_1_1 = parts_1.next()) {
|
|
99
|
-
var part = parts_1_1.value;
|
|
100
|
-
result += String(part.value);
|
|
101
80
|
}
|
|
102
81
|
}
|
|
103
|
-
catch (
|
|
82
|
+
catch (e_1_1) { e_1 = { error: e_1_1 }; }
|
|
104
83
|
finally {
|
|
105
84
|
try {
|
|
106
|
-
if (
|
|
85
|
+
if (nodes_1_1 && !nodes_1_1.done && (_a = nodes_1.return)) _a.call(nodes_1);
|
|
107
86
|
}
|
|
108
|
-
finally { if (
|
|
87
|
+
finally { if (e_1) throw e_1.error; }
|
|
109
88
|
}
|
|
110
89
|
return result;
|
|
111
90
|
};
|
|
112
|
-
/**
|
|
113
|
-
* Returns the same element with any formatting tags removed. If the element
|
|
114
|
-
* itself is a tag, the children will be hoisted and returned, otherwise the
|
|
115
|
-
* element is returned as an array of itself.
|
|
116
|
-
*/
|
|
117
|
-
InternalIntlMessage.prototype._removeRichTags = function (element) {
|
|
118
|
-
var e_3, _a;
|
|
119
|
-
if (element.type === icu_messageformat_parser_1.TYPE.tag) {
|
|
120
|
-
var childContent = [];
|
|
121
|
-
try {
|
|
122
|
-
for (var _b = __values(element.children), _c = _b.next(); !_c.done; _c = _b.next()) {
|
|
123
|
-
var child = _c.value;
|
|
124
|
-
childContent.push.apply(childContent, __spreadArray([], __read(this._removeRichTags(child)), false));
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
catch (e_3_1) { e_3 = { error: e_3_1 }; }
|
|
128
|
-
finally {
|
|
129
|
-
try {
|
|
130
|
-
if (_c && !_c.done && (_a = _b.return)) _a.call(_b);
|
|
131
|
-
}
|
|
132
|
-
finally { if (e_3) throw e_3.error; }
|
|
133
|
-
}
|
|
134
|
-
return childContent;
|
|
135
|
-
}
|
|
136
|
-
else {
|
|
137
|
-
return [element];
|
|
138
|
-
}
|
|
139
|
-
};
|
|
140
91
|
return InternalIntlMessage;
|
|
141
92
|
}());
|
|
142
93
|
exports.InternalIntlMessage = InternalIntlMessage;
|
package/dist/types.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { InternalIntlMessage } from './message';
|
|
2
2
|
import type { IntlMessageGetterAdditions } from './message-loader';
|
|
3
|
+
import { FormatBuilderConstructor } from './format';
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Use this function to define messages as part of the `@discord/intl`
|
|
@@ -11,14 +12,6 @@ import type { IntlMessageGetterAdditions } from './message-loader';
|
|
|
11
12
|
*/
|
|
12
13
|
export declare function defineMessages(messages: object): object;
|
|
13
14
|
|
|
14
|
-
/**
|
|
15
|
-
* A map of tag names to render functions to apply when the tags are
|
|
16
|
-
* encountered while formatting a message. Each `IntlManager` can be
|
|
17
|
-
* constructed with a different map of these elements, for example to allow
|
|
18
|
-
* one manager to render React output while another renders accessibility text.
|
|
19
|
-
*/
|
|
20
|
-
export type RichTextElementMap = Record<string, (chunks: any[]) => any>;
|
|
21
|
-
|
|
22
15
|
/**
|
|
23
16
|
* Placeholder type representing that this property is a Hook to be rendered
|
|
24
17
|
* while formatting.
|
|
@@ -46,19 +39,8 @@ export type IntlTime = string | number | Date | null | undefined;
|
|
|
46
39
|
* strings, and are able to skip multiple formatting steps to work more
|
|
47
40
|
* efficiently. This type represents those messages distinctly.
|
|
48
41
|
*/
|
|
49
|
-
type PlainIntlMessage = string;
|
|
50
|
-
|
|
51
|
-
* A richly-formatted message that injects dynamic values while formatting,
|
|
52
|
-
* with the types of those values defined by the `FormatValues` object. All
|
|
53
|
-
* messages of this type must have at least one value in the object, otherwise
|
|
54
|
-
* the message should be typed as a `PlainIntlMessage`.
|
|
55
|
-
*/
|
|
56
|
-
type TypedIntlMessage<FormatValues extends object> = InternalIntlMessage & {
|
|
57
|
-
__values: FormatValues;
|
|
58
|
-
};
|
|
59
|
-
|
|
60
|
-
type AnyIntlMessage<FormatValues extends object | undefined = undefined> =
|
|
61
|
-
FormatValues extends undefined ? PlainIntlMessage : TypedIntlMessage<FormatValues>;
|
|
42
|
+
export type PlainIntlMessage = string;
|
|
43
|
+
export type AnyIntlMessage = PlainIntlMessage | InternalIntlMessage;
|
|
62
44
|
|
|
63
45
|
export interface IntlMessageGetter extends IntlMessageGetterAdditions {
|
|
64
46
|
(locale: string): InternalIntlMessage | PlainIntlMessage;
|
|
@@ -85,8 +67,8 @@ export interface TypedIntlMessageGetter<FormatValues extends object | undefined>
|
|
|
85
67
|
// from the declared type. The latter of these is definitely semantically
|
|
86
68
|
// correct, but the former is more just an annoying part of the type system
|
|
87
69
|
// because of this returned type being nested within this interface and the
|
|
88
|
-
// strict
|
|
89
|
-
(locale: string): InternalIntlMessage
|
|
70
|
+
// strict subtyping that TypeScript uses.
|
|
71
|
+
(locale: string): InternalIntlMessage;
|
|
90
72
|
|
|
91
73
|
/**
|
|
92
74
|
* A phantom type (not part of the actual typing of the object) that ensures
|
|
@@ -183,7 +165,7 @@ type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (
|
|
|
183
165
|
* checked against the provided message types, and error messages will be
|
|
184
166
|
* accurate as a result.
|
|
185
167
|
*/
|
|
186
|
-
type FormatValuesFor<T> =
|
|
168
|
+
export type FormatValuesFor<T> =
|
|
187
169
|
// `[]` syntax forces TypeScript to not distribute the possible union of
|
|
188
170
|
// types, meaning this represents "_all_ values of T are an extension of
|
|
189
171
|
// the parent type", which in this case represents messages with no
|
|
@@ -209,12 +191,18 @@ type FormatValuesFor<T> =
|
|
|
209
191
|
* the caller to inject React elements within a string, while an AST formatter
|
|
210
192
|
* might require a specific object shape, or allow any plain object.
|
|
211
193
|
*/
|
|
212
|
-
interface FunctionTypeMap {
|
|
194
|
+
export interface FunctionTypeMap {
|
|
213
195
|
link: any;
|
|
214
196
|
hook: any;
|
|
215
197
|
handler: any;
|
|
216
198
|
}
|
|
217
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[]);
|
|
203
|
+
handler: undefined | HandlerType;
|
|
204
|
+
}
|
|
205
|
+
|
|
218
206
|
type ReplaceKeepingNullability<T, P> = P | Exclude<T, NonNullable<T>>;
|
|
219
207
|
|
|
220
208
|
/**
|
|
@@ -226,6 +214,8 @@ type ReplaceKeepingNullability<T, P> = P | Exclude<T, NonNullable<T>>;
|
|
|
226
214
|
type MapFunctionTypes<T, FunctionTypes extends FunctionTypeMap> = {
|
|
227
215
|
[K in keyof T]: ReplaceKeepingNullability<
|
|
228
216
|
T[K],
|
|
217
|
+
// TODO: replace these types with string literal types in the generated
|
|
218
|
+
// file, turning this whole chain into `FunctionTypes[T[K]] ?? T[K]`.
|
|
229
219
|
NonNullable<T[K]> extends LinkFunction
|
|
230
220
|
? FunctionTypes['link']
|
|
231
221
|
: NonNullable<T[K]> extends HookFunction
|
|
@@ -251,7 +241,7 @@ type CheckStrictAny<Check, Yes, No> = (Check extends never ? true : false) exten
|
|
|
251
241
|
* // Result is `{name: string}`
|
|
252
242
|
* ```
|
|
253
243
|
*/
|
|
254
|
-
type IntlMessageGetterInnerType<T extends IntlMessageGetter> =
|
|
244
|
+
export type IntlMessageGetterInnerType<T extends IntlMessageGetter> =
|
|
255
245
|
T extends TypedIntlMessageGetter<infer U> ? U : never;
|
|
256
246
|
|
|
257
247
|
/**
|
|
@@ -260,13 +250,12 @@ type IntlMessageGetterInnerType<T extends IntlMessageGetter> =
|
|
|
260
250
|
* types from `FunctionTypes`. The result is the set of required values
|
|
261
251
|
* necessary to format the message of type `T` with the calling formatter.
|
|
262
252
|
*/
|
|
263
|
-
type RequiredFormatValues<
|
|
253
|
+
export type RequiredFormatValues<
|
|
264
254
|
Values extends IntlMessageGetter,
|
|
265
|
-
DefaultElements,
|
|
266
255
|
FunctionTypes extends FunctionTypeMap,
|
|
267
256
|
> =
|
|
268
257
|
IntlMessageGetterInnerType<Values> extends undefined
|
|
269
|
-
?
|
|
258
|
+
? {}
|
|
270
259
|
: CheckStrictAny<
|
|
271
260
|
IntlMessageGetterInnerType<Values>,
|
|
272
261
|
// If `T` is explicitly `any`, then individual properties can't be typed and
|
|
@@ -277,23 +266,30 @@ type RequiredFormatValues<
|
|
|
277
266
|
// value and turn every property into the mapped `LinkFunction` type, which
|
|
278
267
|
// is almost always going to be wrong.
|
|
279
268
|
any,
|
|
280
|
-
MapFunctionTypes<
|
|
281
|
-
Omit<FormatValuesFor<IntlMessageGetterInnerType<Values>>, keyof DefaultElements>,
|
|
282
|
-
FunctionTypes
|
|
283
|
-
>
|
|
269
|
+
MapFunctionTypes<FormatValuesFor<IntlMessageGetterInnerType<Values>>, FunctionTypes>
|
|
284
270
|
>;
|
|
285
271
|
|
|
286
|
-
export interface
|
|
287
|
-
$_:
|
|
288
|
-
$b:
|
|
289
|
-
$i:
|
|
290
|
-
$p:
|
|
291
|
-
$link:
|
|
292
|
-
$code:
|
|
272
|
+
export interface RichTextFormattingMap<T = any> {
|
|
273
|
+
$_: T;
|
|
274
|
+
$b: T;
|
|
275
|
+
$i: T;
|
|
276
|
+
$p: T;
|
|
277
|
+
$link: T;
|
|
278
|
+
$code: T;
|
|
293
279
|
}
|
|
294
280
|
|
|
295
|
-
export type
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
FunctionTypeMap
|
|
299
|
-
|
|
281
|
+
export type RichTextTagNames = keyof RichTextFormattingMap;
|
|
282
|
+
|
|
283
|
+
export interface FormatterImplementation<
|
|
284
|
+
FunctionTypes extends FunctionTypeMap,
|
|
285
|
+
Result,
|
|
286
|
+
BuilderResult = Result,
|
|
287
|
+
> {
|
|
288
|
+
format: (
|
|
289
|
+
message: AnyIntlMessage,
|
|
290
|
+
values: object,
|
|
291
|
+
builder?: FormatBuilderConstructor<BuilderResult>,
|
|
292
|
+
) => Result;
|
|
293
|
+
builder: FormatBuilderConstructor<BuilderResult extends (infer T)[] ? T : BuilderResult>;
|
|
294
|
+
__$functionTypes?: FunctionTypes;
|
|
295
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@discord/intl",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "Client runtime for managing messages and translations in a React project.",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -15,7 +15,6 @@
|
|
|
15
15
|
"src"
|
|
16
16
|
],
|
|
17
17
|
"dependencies": {
|
|
18
|
-
"@formatjs/icu-messageformat-parser": "^2.7.6",
|
|
19
18
|
"@formatjs/intl": "^2.10.1",
|
|
20
19
|
"intl-messageformat": "^10.5.11"
|
|
21
20
|
},
|
package/src/format.ts
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The core formatter, responsible for applying values to placeholders in
|
|
3
|
+
* messages and creating a static CST from the result. This method does _not_
|
|
4
|
+
* replace any of the rich-text formatting values (e.g., `$b` or `$link`) and
|
|
5
|
+
* leaves those to final formatters to apply as appropriate for the context
|
|
6
|
+
* of where the message is being rendered (e.g., React versus a plain string).
|
|
7
|
+
*
|
|
8
|
+
* The majority of this method is lifted from FormatJS' own `formatToParts`
|
|
9
|
+
* implementation, but is abstracted here to provide a few benefits:
|
|
10
|
+
* - working with the keyless AST format rather than having to transpose to the
|
|
11
|
+
* exact object format that FormatJS expects.
|
|
12
|
+
* - understanding the difference between rich text formatting values and
|
|
13
|
+
* user-provided values.
|
|
14
|
+
* - provide extra options related to the above for more control over how
|
|
15
|
+
* formatting operates, while abstracting out some of the common values.
|
|
16
|
+
* - adding extra context for hook and link functions to provide a `key`
|
|
17
|
+
* parameter, important to have React treat the resulting elements nicely.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { FormatJsNode, FormatJsNodeType } from './keyless-json';
|
|
21
|
+
import { Formats, Formatters, MissingValueError } from 'intl-messageformat';
|
|
22
|
+
import { RichTextTagNames } from './types';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Returns true if the tag name should be considered a rich text tag that
|
|
26
|
+
* applies formatting to a message, rather than being a user-supplied value.
|
|
27
|
+
*/
|
|
28
|
+
function isRichTextTag(name: string) {
|
|
29
|
+
return name[0] === '$';
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export abstract class FormatBuilder<Result> {
|
|
33
|
+
abstract pushRichTextTag(tag: RichTextTagNames, children: Result[]): void;
|
|
34
|
+
abstract pushLiteralText(text: string): void;
|
|
35
|
+
abstract pushObject(value: object): void;
|
|
36
|
+
abstract finish(): Result[];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export type FormatBuilderConstructor<Result> = new () => FormatBuilder<Result>;
|
|
40
|
+
|
|
41
|
+
export function bindFormatValuesWithBuilder<T, Builder extends FormatBuilder<T>>(
|
|
42
|
+
builder: Builder,
|
|
43
|
+
nodes: FormatJsNode[],
|
|
44
|
+
locales: string | string[],
|
|
45
|
+
formatters: Formatters,
|
|
46
|
+
formats: Formats,
|
|
47
|
+
values: Record<string, string | object> = {},
|
|
48
|
+
currentPluralValue?: number,
|
|
49
|
+
originalMessage?: string,
|
|
50
|
+
) {
|
|
51
|
+
// Hot path for static messages that are just parsed as a single string element.
|
|
52
|
+
if (nodes.length === 1 && nodes[0].type === FormatJsNodeType.Literal) {
|
|
53
|
+
builder.pushLiteralText(nodes[0].value);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
for (const node of nodes) {
|
|
58
|
+
switch (node.type) {
|
|
59
|
+
case FormatJsNodeType.Literal:
|
|
60
|
+
builder.pushLiteralText(node.value);
|
|
61
|
+
continue;
|
|
62
|
+
|
|
63
|
+
case FormatJsNodeType.Pound:
|
|
64
|
+
// Replace `#` in plural rules with the actual numeric value. Only
|
|
65
|
+
// numeric values are replaced, otherwise the value is completed ignored?
|
|
66
|
+
// Behavior copied from FormatJS directly.
|
|
67
|
+
if (typeof currentPluralValue === 'number') {
|
|
68
|
+
const value = formatters.getNumberFormat(locales).format(currentPluralValue);
|
|
69
|
+
builder.pushLiteralText(value);
|
|
70
|
+
}
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const { value: variableName } = node;
|
|
75
|
+
// Enforce that all required values are provided by the caller, even if the
|
|
76
|
+
// actual value is falsy/undefined.
|
|
77
|
+
if (!(variableName in values) && !isRichTextTag(variableName)) {
|
|
78
|
+
throw new MissingValueError(variableName, originalMessage);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const value = values[variableName];
|
|
82
|
+
|
|
83
|
+
switch (node.type) {
|
|
84
|
+
case FormatJsNodeType.Argument:
|
|
85
|
+
// Empty values don't need to be added at all, they are purely for AST representation.
|
|
86
|
+
if (variableName == '$_') break;
|
|
87
|
+
|
|
88
|
+
if (typeof value === 'object') {
|
|
89
|
+
builder.pushObject(value);
|
|
90
|
+
} else {
|
|
91
|
+
// Taken from FormatJS: non-objects (strings, numbers, and falsy
|
|
92
|
+
// values) all get cast to strings immediately as literal nodes.
|
|
93
|
+
builder.pushLiteralText(String(value));
|
|
94
|
+
}
|
|
95
|
+
break;
|
|
96
|
+
case FormatJsNodeType.Date: {
|
|
97
|
+
// Distinct from FormatJS: We don't currently parse the skeleton ahead of time in the AST,
|
|
98
|
+
// so this manages parsing the skeleton as well before passing it onto the date formatter.
|
|
99
|
+
if (typeof node.style !== 'string') {
|
|
100
|
+
throw '@discord/intl does not currently support non-string format skeletons';
|
|
101
|
+
}
|
|
102
|
+
const style = node.style in formats.date ? formats.date[node.style] : undefined; // TODO: parseSkeleton();
|
|
103
|
+
// @ts-expect-error Cast string values to dates properly.
|
|
104
|
+
builder.pushLiteralText(formatters.getDateTimeFormat(locales, style).format(value));
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
case FormatJsNodeType.Time: {
|
|
108
|
+
// Distinct from FormatJS: We don't currently parse the skeleton ahead of time in the AST,
|
|
109
|
+
// so this manages parsing the skeleton as well before passing it onto the date formatter.
|
|
110
|
+
if (typeof node.style !== 'string') {
|
|
111
|
+
throw '@discord/intl does not currently support non-string format skeletons';
|
|
112
|
+
}
|
|
113
|
+
const style = node.style in formats.time ? formats.time[node.style] : undefined; // TODO: parseSkeleton();
|
|
114
|
+
builder.pushLiteralText(
|
|
115
|
+
// @ts-expect-error Cast string values to dates properly.
|
|
116
|
+
formatters.getDateTimeFormat(locales, style).format(value),
|
|
117
|
+
);
|
|
118
|
+
break;
|
|
119
|
+
}
|
|
120
|
+
case FormatJsNodeType.Number: {
|
|
121
|
+
// Distinct from FormatJS: We don't currently parse the skeleton ahead of time in the AST,
|
|
122
|
+
// so this manages parsing the skeleton as well before passing it onto the date formatter.
|
|
123
|
+
if (typeof node.style !== 'string') {
|
|
124
|
+
throw '@discord/intl does not currently support non-string format skeletons';
|
|
125
|
+
}
|
|
126
|
+
const style = node.style in formats.number ? formats.number[node.style] : undefined; // TODO: parseSkeleton();
|
|
127
|
+
|
|
128
|
+
// @ts-expect-error Support `scale` style property.
|
|
129
|
+
const scaledValue = value as number;
|
|
130
|
+
// const scaledValue = (value as number) * (style?.scale ?? 1);
|
|
131
|
+
builder.pushLiteralText(formatters.getNumberFormat(locales, style).format(scaledValue));
|
|
132
|
+
break;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
case FormatJsNodeType.Tag: {
|
|
136
|
+
const { children } = node;
|
|
137
|
+
const appliedChildren = bindFormatValues(
|
|
138
|
+
builder.constructor as FormatBuilderConstructor<T>,
|
|
139
|
+
children,
|
|
140
|
+
locales,
|
|
141
|
+
formatters,
|
|
142
|
+
formats,
|
|
143
|
+
values,
|
|
144
|
+
currentPluralValue,
|
|
145
|
+
);
|
|
146
|
+
if (isRichTextTag(variableName)) {
|
|
147
|
+
builder.pushRichTextTag(variableName as RichTextTagNames, appliedChildren);
|
|
148
|
+
} else {
|
|
149
|
+
if (typeof value !== 'function') {
|
|
150
|
+
throw `expected a function type for a Tag formatting value, ${variableName}. got ${typeof value}: ${value}`;
|
|
151
|
+
}
|
|
152
|
+
let chunks = value(appliedChildren);
|
|
153
|
+
chunks = Array.isArray(chunks) ? chunks : [chunks];
|
|
154
|
+
for (const chunk of chunks) {
|
|
155
|
+
if (typeof chunk === 'string') {
|
|
156
|
+
builder.pushLiteralText(chunk);
|
|
157
|
+
} else {
|
|
158
|
+
builder.pushObject(chunk);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
break;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
case FormatJsNodeType.Select: {
|
|
166
|
+
const option = node.value in node.options ? node.options[node.value] : node.options.other;
|
|
167
|
+
if (option == null) {
|
|
168
|
+
throw `${node.value} is not a known option for select value ${variableName}. Valid options are ${Object.keys(node.options).join(', ')}`;
|
|
169
|
+
}
|
|
170
|
+
bindFormatValuesWithBuilder(builder, option.value, locales, formatters, formats, values);
|
|
171
|
+
break;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
case FormatJsNodeType.Plural: {
|
|
175
|
+
const option = (() => {
|
|
176
|
+
const exactSelector = `=${value}`;
|
|
177
|
+
if (exactSelector in node.options) return node.options[exactSelector];
|
|
178
|
+
const rule = formatters
|
|
179
|
+
.getPluralRules(locales, { type: node.pluralType })
|
|
180
|
+
// @ts-expect-error Assert this `as number` properly.
|
|
181
|
+
.select((value as number) - (node.offset ?? 0));
|
|
182
|
+
return node.options[rule] ?? node.options.other;
|
|
183
|
+
})();
|
|
184
|
+
|
|
185
|
+
if (option == null) {
|
|
186
|
+
throw `${node.value} is not a known option for plural value ${variableName}. Valid options are ${Object.keys(node.options).join(', ')}`;
|
|
187
|
+
}
|
|
188
|
+
bindFormatValuesWithBuilder(
|
|
189
|
+
builder,
|
|
190
|
+
option.value,
|
|
191
|
+
locales,
|
|
192
|
+
formatters,
|
|
193
|
+
formats,
|
|
194
|
+
values,
|
|
195
|
+
// @ts-expect-error assert this `as number` properly.
|
|
196
|
+
(value as number) - (node.offset ?? 0),
|
|
197
|
+
);
|
|
198
|
+
break;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export function bindFormatValues<Result>(
|
|
205
|
+
Builder: FormatBuilderConstructor<Result>,
|
|
206
|
+
nodes: FormatJsNode[],
|
|
207
|
+
locales: string | string[],
|
|
208
|
+
formatters: Formatters,
|
|
209
|
+
formats: Formats,
|
|
210
|
+
values: Record<string, string | object> = {},
|
|
211
|
+
currentPluralValue?: number,
|
|
212
|
+
): Result[] {
|
|
213
|
+
const builder = new Builder();
|
|
214
|
+
bindFormatValuesWithBuilder(
|
|
215
|
+
builder,
|
|
216
|
+
nodes,
|
|
217
|
+
locales,
|
|
218
|
+
formatters,
|
|
219
|
+
formats,
|
|
220
|
+
values,
|
|
221
|
+
currentPluralValue,
|
|
222
|
+
);
|
|
223
|
+
return builder.finish();
|
|
224
|
+
}
|