@discord/intl 0.2.1-rc.2 → 0.2.2-canary.96b50b6
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 +23 -19
- package/dist/formatters/ast.d.ts +7 -3
- package/dist/formatters/ast.js +6 -4
- package/dist/formatters/index.d.ts +1 -1
- package/dist/formatters/markdown.js +2 -1
- package/dist/formatters/react.d.ts +2 -2
- package/dist/formatters/react.js +4 -3
- package/dist/formatters/string.js +1 -1
- package/dist/hash.d.ts +8 -0
- package/dist/hash.js +41 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +6 -1
- package/dist/intl-manager.d.ts +10 -0
- package/dist/intl-manager.js +22 -5
- package/dist/message.d.ts +5 -0
- package/dist/message.js +175 -0
- package/dist/types.d.ts +1 -0
- package/package.json +5 -3
- package/src/format.ts +26 -16
- package/src/formatters/ast.ts +10 -4
- package/src/formatters/index.ts +1 -0
- package/src/formatters/markdown.ts +2 -1
- package/src/formatters/react.ts +7 -7
- package/src/formatters/string.ts +1 -1
- package/src/hash.ts +42 -0
- package/src/index.ts +3 -1
- package/src/intl-manager.ts +23 -5
- package/src/message.ts +119 -1
- package/src/types.d.ts +1 -0
package/dist/format.js
CHANGED
|
@@ -34,6 +34,7 @@ exports.bindFormatValuesWithBuilder = bindFormatValuesWithBuilder;
|
|
|
34
34
|
exports.bindFormatValues = bindFormatValues;
|
|
35
35
|
var keyless_json_1 = require("./keyless-json");
|
|
36
36
|
var intl_messageformat_1 = require("intl-messageformat");
|
|
37
|
+
var icu_skeleton_parser_1 = require("@formatjs/icu-skeleton-parser");
|
|
37
38
|
/**
|
|
38
39
|
* Returns true if the tag name should be considered a rich text tag that
|
|
39
40
|
* applies formatting to a message, rather than being a user-supplied value.
|
|
@@ -49,7 +50,7 @@ var FormatBuilder = /** @class */ (function () {
|
|
|
49
50
|
exports.FormatBuilder = FormatBuilder;
|
|
50
51
|
function bindFormatValuesWithBuilder(builder, nodes, locales, formatters, formats, values, currentPluralValue, originalMessage) {
|
|
51
52
|
var e_1, _a;
|
|
52
|
-
var _b;
|
|
53
|
+
var _b, _c;
|
|
53
54
|
if (values === void 0) { values = {}; }
|
|
54
55
|
// Hot path for static messages that are just parsed as a single string element.
|
|
55
56
|
if (nodes.length === 1 && nodes[0].type === keyless_json_1.FormatJsNodeType.Literal) {
|
|
@@ -57,7 +58,7 @@ function bindFormatValuesWithBuilder(builder, nodes, locales, formatters, format
|
|
|
57
58
|
return;
|
|
58
59
|
}
|
|
59
60
|
var _loop_1 = function (node) {
|
|
60
|
-
var e_2,
|
|
61
|
+
var e_2, _d;
|
|
61
62
|
switch (node.type) {
|
|
62
63
|
case keyless_json_1.FormatJsNodeType.Literal:
|
|
63
64
|
builder.pushLiteralText(node.value);
|
|
@@ -96,10 +97,11 @@ function bindFormatValuesWithBuilder(builder, nodes, locales, formatters, format
|
|
|
96
97
|
case keyless_json_1.FormatJsNodeType.Date: {
|
|
97
98
|
// Distinct from FormatJS: We don't currently parse the skeleton ahead of time in the AST,
|
|
98
99
|
// so this manages parsing the skeleton as well before passing it onto the date formatter.
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
100
|
+
var style = node.style in formats.date
|
|
101
|
+
? formats.date[node.style]
|
|
102
|
+
: node.style != null
|
|
103
|
+
? (0, icu_skeleton_parser_1.parseDateTimeSkeleton)(node.style)
|
|
104
|
+
: undefined;
|
|
103
105
|
// @ts-expect-error Cast string values to dates properly.
|
|
104
106
|
builder.pushLiteralText(formatters.getDateTimeFormat(locales, style).format(value));
|
|
105
107
|
break;
|
|
@@ -107,10 +109,11 @@ function bindFormatValuesWithBuilder(builder, nodes, locales, formatters, format
|
|
|
107
109
|
case keyless_json_1.FormatJsNodeType.Time: {
|
|
108
110
|
// Distinct from FormatJS: We don't currently parse the skeleton ahead of time in the AST,
|
|
109
111
|
// so this manages parsing the skeleton as well before passing it onto the date formatter.
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
112
|
+
var style = node.style in formats.time
|
|
113
|
+
? formats.time[node.style]
|
|
114
|
+
: node.style != null
|
|
115
|
+
? (0, icu_skeleton_parser_1.parseDateTimeSkeleton)(node.style)
|
|
116
|
+
: undefined; // TODO: parseSkeleton();
|
|
114
117
|
builder.pushLiteralText(
|
|
115
118
|
// @ts-expect-error Cast string values to dates properly.
|
|
116
119
|
formatters.getDateTimeFormat(locales, style).format(value));
|
|
@@ -119,13 +122,14 @@ function bindFormatValuesWithBuilder(builder, nodes, locales, formatters, format
|
|
|
119
122
|
case keyless_json_1.FormatJsNodeType.Number: {
|
|
120
123
|
// Distinct from FormatJS: We don't currently parse the skeleton ahead of time in the AST,
|
|
121
124
|
// so this manages parsing the skeleton as well before passing it onto the date formatter.
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
var scaledValue =
|
|
128
|
-
//
|
|
125
|
+
var style = node.style in formats.number
|
|
126
|
+
? formats.number[node.style]
|
|
127
|
+
: node.style != null
|
|
128
|
+
? (0, icu_skeleton_parser_1.parseNumberSkeleton)((0, icu_skeleton_parser_1.parseNumberSkeletonFromString)(node.style))
|
|
129
|
+
: undefined;
|
|
130
|
+
var scaledValue =
|
|
131
|
+
// @ts-expect-error This is a weird cast that's not accurate, but works in the short term.
|
|
132
|
+
typeof value !== 'number' ? value : ((_b = value * (style === null || style === void 0 ? void 0 : style.scale)) !== null && _b !== void 0 ? _b : 1);
|
|
129
133
|
builder.pushLiteralText(formatters.getNumberFormat(locales, style).format(scaledValue));
|
|
130
134
|
break;
|
|
131
135
|
}
|
|
@@ -155,7 +159,7 @@ function bindFormatValuesWithBuilder(builder, nodes, locales, formatters, format
|
|
|
155
159
|
catch (e_2_1) { e_2 = { error: e_2_1 }; }
|
|
156
160
|
finally {
|
|
157
161
|
try {
|
|
158
|
-
if (chunks_1_1 && !chunks_1_1.done && (
|
|
162
|
+
if (chunks_1_1 && !chunks_1_1.done && (_d = chunks_1.return)) _d.call(chunks_1);
|
|
159
163
|
}
|
|
160
164
|
finally { if (e_2) throw e_2.error; }
|
|
161
165
|
}
|
|
@@ -187,7 +191,7 @@ function bindFormatValuesWithBuilder(builder, nodes, locales, formatters, format
|
|
|
187
191
|
}
|
|
188
192
|
bindFormatValuesWithBuilder(builder, option.value, locales, formatters, formats, values,
|
|
189
193
|
// @ts-expect-error assert this `as number` properly.
|
|
190
|
-
value - ((
|
|
194
|
+
value - ((_c = node.offset) !== null && _c !== void 0 ? _c : 0));
|
|
191
195
|
break;
|
|
192
196
|
}
|
|
193
197
|
}
|
package/dist/formatters/ast.d.ts
CHANGED
|
@@ -8,8 +8,9 @@ export type AstFunctionTypes = FunctionTypes<RichTextNode, object>;
|
|
|
8
8
|
export declare enum RichTextNodeType {
|
|
9
9
|
Text = "text",
|
|
10
10
|
Strong = "strong",
|
|
11
|
-
Emphasis = "
|
|
12
|
-
|
|
11
|
+
Emphasis = "em",
|
|
12
|
+
Strikethrough = "s",
|
|
13
|
+
Code = "inlineCode",
|
|
13
14
|
Link = "link",
|
|
14
15
|
Paragraph = "paragraph",
|
|
15
16
|
/**
|
|
@@ -36,6 +37,9 @@ interface RichTextStrongNode extends RichTextNodeBase<RichTextNode[]> {
|
|
|
36
37
|
interface RichTextEmphasisNode extends RichTextNodeBase<RichTextNode[]> {
|
|
37
38
|
type: RichTextNodeType.Emphasis;
|
|
38
39
|
}
|
|
40
|
+
interface RichTextStrikethroughNode extends RichTextNodeBase<RichTextNode[]> {
|
|
41
|
+
type: RichTextNodeType.Strikethrough;
|
|
42
|
+
}
|
|
39
43
|
interface RichTextCodeNode extends RichTextNodeBase<RichTextNode[]> {
|
|
40
44
|
type: RichTextNodeType.Code;
|
|
41
45
|
}
|
|
@@ -49,7 +53,7 @@ interface RichTextLinkNode extends RichTextNodeBase<RichTextNode[]> {
|
|
|
49
53
|
interface RichTextObjectNode extends RichTextNodeBase<object> {
|
|
50
54
|
type: RichTextNodeType.Object;
|
|
51
55
|
}
|
|
52
|
-
export type RichTextNode = RichTextTextNode | RichTextStrongNode | RichTextEmphasisNode | RichTextCodeNode | RichTextParagraphNode | RichTextLinkNode | RichTextObjectNode;
|
|
56
|
+
export type RichTextNode = RichTextTextNode | RichTextStrongNode | RichTextEmphasisNode | RichTextStrikethroughNode | RichTextCodeNode | RichTextParagraphNode | RichTextLinkNode | RichTextObjectNode;
|
|
53
57
|
/**
|
|
54
58
|
* Format the given message in the current locale with the provided values.
|
|
55
59
|
* The returned values is _always_ an Array of parts, even if the message is
|
package/dist/formatters/ast.js
CHANGED
|
@@ -50,8 +50,9 @@ var RichTextNodeType;
|
|
|
50
50
|
(function (RichTextNodeType) {
|
|
51
51
|
RichTextNodeType["Text"] = "text";
|
|
52
52
|
RichTextNodeType["Strong"] = "strong";
|
|
53
|
-
RichTextNodeType["Emphasis"] = "
|
|
54
|
-
RichTextNodeType["
|
|
53
|
+
RichTextNodeType["Emphasis"] = "em";
|
|
54
|
+
RichTextNodeType["Strikethrough"] = "s";
|
|
55
|
+
RichTextNodeType["Code"] = "inlineCode";
|
|
55
56
|
RichTextNodeType["Link"] = "link";
|
|
56
57
|
RichTextNodeType["Paragraph"] = "paragraph";
|
|
57
58
|
/**
|
|
@@ -68,8 +69,9 @@ var RichTextNodeType;
|
|
|
68
69
|
var AST_RICH_TEXT_ELEMENTS = {
|
|
69
70
|
$_: function () { return ({ type: RichTextNodeType.Text, content: '' }); },
|
|
70
71
|
$b: function (content) { return ({ type: RichTextNodeType.Strong, content: content }); },
|
|
71
|
-
$code: function (content) { return ({ type: RichTextNodeType.Code, content: content }); },
|
|
72
72
|
$i: function (content) { return ({ type: RichTextNodeType.Emphasis, content: content }); },
|
|
73
|
+
$del: function (content) { return ({ type: RichTextNodeType.Strikethrough, content: content }); },
|
|
74
|
+
$code: function (content) { return ({ type: RichTextNodeType.Code, content: content }); },
|
|
73
75
|
$link: function (_a) {
|
|
74
76
|
var _b = __read(_a), target = _b[0], content = _b.slice(1);
|
|
75
77
|
return ({
|
|
@@ -102,7 +104,7 @@ var AstBuilder = /** @class */ (function (_super) {
|
|
|
102
104
|
};
|
|
103
105
|
AstBuilder.prototype.pushLiteralText = function (text) {
|
|
104
106
|
var last = this.result[this.result.length - 1];
|
|
105
|
-
if (last.type === RichTextNodeType.Text) {
|
|
107
|
+
if (last != null && last.type === RichTextNodeType.Text) {
|
|
106
108
|
last.content += text;
|
|
107
109
|
}
|
|
108
110
|
else {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export { astFormatter, type AstFunctionTypes, type RichTextNode, RichTextNodeType } from './ast';
|
|
2
2
|
export { markdownFormatter, type MarkdownFunctionTypes } from './markdown';
|
|
3
|
-
export { reactFormatter, makeReactFormatter, DEFAULT_REACT_RICH_TEXT_ELEMENTS, type ReactFunctionTypes, } from './react';
|
|
3
|
+
export { reactFormatter, makeReactFormatter, DEFAULT_REACT_RICH_TEXT_ELEMENTS, type ReactFunctionTypes, ReactIntlMessage, } from './react';
|
|
4
4
|
export { stringFormatter, type StringFunctionTypes } from './string';
|
|
@@ -46,8 +46,9 @@ var string_1 = require("./string");
|
|
|
46
46
|
var MARKDOWN_RICH_TEXT_ELEMENTS = {
|
|
47
47
|
$_: function () { return ''; },
|
|
48
48
|
$b: function (content) { return '**' + content.join('') + '**'; },
|
|
49
|
-
$code: function (content) { return '`' + content.join('') + '`'; },
|
|
50
49
|
$i: function (content) { return '*' + content.join('') + '*'; },
|
|
50
|
+
$del: function (content) { return '~~' + content.join('') + '~~'; },
|
|
51
|
+
$code: function (content) { return '`' + content.join('') + '`'; },
|
|
51
52
|
$link: function (_a) {
|
|
52
53
|
var _b = __read(_a), target = _b[0], content = _b.slice(1);
|
|
53
54
|
return '[' + content.join('') + '](' + target + ')';
|
|
@@ -18,9 +18,9 @@ export type ReactFunctionTypes = FunctionTypes<React.ReactNode, ReactClickHandle
|
|
|
18
18
|
onContextMenu: ReactClickHandler;
|
|
19
19
|
}>;
|
|
20
20
|
export declare const DEFAULT_REACT_RICH_TEXT_ELEMENTS: RichTextFormattingMap<ReactFunctionTypes['hook']>;
|
|
21
|
-
export type ReactIntlMessage = React.ReactElement
|
|
21
|
+
export type ReactIntlMessage = Array<React.ReactElement | string> & {
|
|
22
22
|
__brand: 'discord-intl';
|
|
23
|
-
}
|
|
23
|
+
};
|
|
24
24
|
export declare function formatReact(this: IntlManager, message: AnyIntlMessage, values: object, Builder: FormatBuilderConstructor<React.ReactElement>): ReactIntlMessage;
|
|
25
25
|
/**
|
|
26
26
|
* Create a new React formatter with the given rich text elements replacing the defaults. Use this
|
package/dist/formatters/react.js
CHANGED
|
@@ -49,8 +49,9 @@ var h = React.createElement;
|
|
|
49
49
|
exports.DEFAULT_REACT_RICH_TEXT_ELEMENTS = {
|
|
50
50
|
$_: function () { return ''; },
|
|
51
51
|
$b: function (content, key) { return h('strong', { key: key }, content); },
|
|
52
|
-
$code: function (content, key) { return h('code', { key: key }, content); },
|
|
53
52
|
$i: function (content, key) { return h('em', { key: key }, content); },
|
|
53
|
+
$del: function (content, key) { return h('del', { key: key }, content); },
|
|
54
|
+
$code: function (content, key) { return h('code', { key: key }, content); },
|
|
54
55
|
// $link will always be [href, <empty>, ...content]
|
|
55
56
|
$link: function (_a, key) {
|
|
56
57
|
var _b = __read(_a), href = _b[0], content = _b.slice(1);
|
|
@@ -93,10 +94,10 @@ function createReactBuilder(richTextElements) {
|
|
|
93
94
|
}
|
|
94
95
|
function formatReact(message, values, Builder) {
|
|
95
96
|
if (typeof message === 'string') {
|
|
96
|
-
return
|
|
97
|
+
return [message];
|
|
97
98
|
}
|
|
98
99
|
var parts = this.bindFormatValues(Builder, message, values);
|
|
99
|
-
return
|
|
100
|
+
return parts;
|
|
100
101
|
}
|
|
101
102
|
/**
|
|
102
103
|
* Create a new React formatter with the given rich text elements replacing the defaults. Use this
|
|
@@ -67,7 +67,7 @@ var StringBuilder = /** @class */ (function (_super) {
|
|
|
67
67
|
StringBuilder.prototype.pushObject = function (value) {
|
|
68
68
|
// Objects are only included in the result if they specify a toString value directly.
|
|
69
69
|
// Otherwise, they would be rendered as `[object Object]`, which is never helpful.
|
|
70
|
-
if ('toString' in value) {
|
|
70
|
+
if (value != null && 'toString' in value) {
|
|
71
71
|
this.result += value.toString();
|
|
72
72
|
}
|
|
73
73
|
};
|
package/dist/hash.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Returns a consistent, short hash of the given key by first processing it through a hash digest,
|
|
3
|
+
* then encoding the first few bytes to base64.
|
|
4
|
+
*
|
|
5
|
+
* This function is specifically written to mirror the native backend hashing function used by
|
|
6
|
+
* `@discord/intl-loader-core`, to be able to hash names at runtime.
|
|
7
|
+
*/
|
|
8
|
+
export declare function runtimeHashMessageKey(key: string): string;
|
package/dist/hash.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.runtimeHashMessageKey = runtimeHashMessageKey;
|
|
4
|
+
var xxhash64_1 = require("@intrnl/xxhash64");
|
|
5
|
+
var BASE64_TABLE = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'.split('');
|
|
6
|
+
var IS_BIG_ENDIAN = (function () {
|
|
7
|
+
var array = new Uint8Array(4);
|
|
8
|
+
var view = new Uint32Array(array.buffer);
|
|
9
|
+
return !((view[0] = 1) & array[0]);
|
|
10
|
+
})();
|
|
11
|
+
function numberToBytes(number) {
|
|
12
|
+
number = BigInt(number);
|
|
13
|
+
var array = [];
|
|
14
|
+
var byteCount = Math.ceil(Math.floor(Math.log2(Number(number)) + 1) / 8);
|
|
15
|
+
for (var i = 0; i < byteCount; i++) {
|
|
16
|
+
array.unshift(Number((number >> BigInt(8 * i)) & BigInt(255)));
|
|
17
|
+
}
|
|
18
|
+
var bytes = new Uint8Array(array);
|
|
19
|
+
// The native `hashToMessageKey` always works in Big/Network Endian bytes, so this array
|
|
20
|
+
// needs to be converted to the same endianness to get the same base64 result.
|
|
21
|
+
return IS_BIG_ENDIAN ? bytes : bytes.reverse();
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Returns a consistent, short hash of the given key by first processing it through a hash digest,
|
|
25
|
+
* then encoding the first few bytes to base64.
|
|
26
|
+
*
|
|
27
|
+
* This function is specifically written to mirror the native backend hashing function used by
|
|
28
|
+
* `@discord/intl-loader-core`, to be able to hash names at runtime.
|
|
29
|
+
*/
|
|
30
|
+
function runtimeHashMessageKey(key) {
|
|
31
|
+
var hash = (0, xxhash64_1.hash)(key, 0);
|
|
32
|
+
var bytes = numberToBytes(hash);
|
|
33
|
+
return [
|
|
34
|
+
BASE64_TABLE[bytes[0] >> 2],
|
|
35
|
+
BASE64_TABLE[((bytes[0] & 0x03) << 4) | (bytes[1] >> 4)],
|
|
36
|
+
BASE64_TABLE[((bytes[1] & 0x0f) << 2) | (bytes[2] >> 6)],
|
|
37
|
+
BASE64_TABLE[bytes[2] & 0x3f],
|
|
38
|
+
BASE64_TABLE[bytes[3] >> 2],
|
|
39
|
+
BASE64_TABLE[((bytes[3] & 0x03) << 4) | (bytes[3] >> 4)],
|
|
40
|
+
].join('');
|
|
41
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
2
|
import { RichTextNode } from './formatters';
|
|
3
3
|
export * from './formatters';
|
|
4
|
+
export { FormatBuilder, FormatBuilderConstructor, bindFormatValues } from './format';
|
|
5
|
+
export { runtimeHashMessageKey } from './hash';
|
|
4
6
|
export { IntlManager, DEFAULT_LOCALE, type FormatFunction } from './intl-manager';
|
|
5
7
|
export { createLoader, loadAllMessagesInLocale, waitForAllDefaultIntlMessagesLoaded, MessageLoader, } from './message-loader';
|
|
6
8
|
export type * from './types.d.ts';
|
|
@@ -19,7 +21,7 @@ export type * from './types.d.ts';
|
|
|
19
21
|
* intl message, even if the actual value comes from elsewhere (like a
|
|
20
22
|
* user-generated string).
|
|
21
23
|
*/
|
|
22
|
-
export type ReactIntlMessage = string | React.ReactElement
|
|
24
|
+
export type ReactIntlMessage = string | React.ReactElement | Array<string | React.ReactElement>;
|
|
23
25
|
/**
|
|
24
26
|
* The return value of `formatToParts` from `@discord/intl`, this type
|
|
25
27
|
* represents any AST structure for a message rendered using this system.
|
package/dist/index.js
CHANGED
|
@@ -14,8 +14,13 @@ 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 = void 0;
|
|
17
|
+
exports.MessageLoader = exports.waitForAllDefaultIntlMessagesLoaded = exports.loadAllMessagesInLocale = exports.createLoader = exports.DEFAULT_LOCALE = exports.IntlManager = exports.runtimeHashMessageKey = exports.bindFormatValues = exports.FormatBuilder = void 0;
|
|
18
18
|
__exportStar(require("./formatters"), exports);
|
|
19
|
+
var format_1 = require("./format");
|
|
20
|
+
Object.defineProperty(exports, "FormatBuilder", { enumerable: true, get: function () { return format_1.FormatBuilder; } });
|
|
21
|
+
Object.defineProperty(exports, "bindFormatValues", { enumerable: true, get: function () { return format_1.bindFormatValues; } });
|
|
22
|
+
var hash_1 = require("./hash");
|
|
23
|
+
Object.defineProperty(exports, "runtimeHashMessageKey", { enumerable: true, get: function () { return hash_1.runtimeHashMessageKey; } });
|
|
19
24
|
var intl_manager_1 = require("./intl-manager");
|
|
20
25
|
Object.defineProperty(exports, "IntlManager", { enumerable: true, get: function () { return intl_manager_1.IntlManager; } });
|
|
21
26
|
Object.defineProperty(exports, "DEFAULT_LOCALE", { enumerable: true, get: function () { return intl_manager_1.DEFAULT_LOCALE; } });
|
package/dist/intl-manager.d.ts
CHANGED
|
@@ -45,6 +45,16 @@ export declare class IntlManager {
|
|
|
45
45
|
* immediately return the plain string value of the message in the current locale.
|
|
46
46
|
*/
|
|
47
47
|
string<T extends TypedIntlMessageGetter<undefined>>(message: T): string;
|
|
48
|
+
/**
|
|
49
|
+
* Return a raw string representing the syntax of the original message, as
|
|
50
|
+
* authored, with no values replaced. The result of this function could be
|
|
51
|
+
* written back to the definition file for the message and re-parsed to
|
|
52
|
+
* create an identical message to the original.
|
|
53
|
+
*
|
|
54
|
+
* This should rarely be necessary outside of sending raw messages to other
|
|
55
|
+
* applications that do their own message parsing.
|
|
56
|
+
*/
|
|
57
|
+
reserialize<T extends IntlMessageGetter>(message: T): string;
|
|
48
58
|
bindFormatValues<T>(Builder: FormatBuilderConstructor<T>, message: InternalIntlMessage, values: Record<string, any>): T[];
|
|
49
59
|
}
|
|
50
60
|
export {};
|
package/dist/intl-manager.js
CHANGED
|
@@ -120,11 +120,28 @@ var IntlManager = /** @class */ (function () {
|
|
|
120
120
|
* immediately return the plain string value of the message in the current locale.
|
|
121
121
|
*/
|
|
122
122
|
IntlManager.prototype.string = function (message) {
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
//
|
|
127
|
-
|
|
123
|
+
var resolved = message(this.currentLocale);
|
|
124
|
+
if (resolved == null || resolved.ast.length === 0)
|
|
125
|
+
return '';
|
|
126
|
+
// TODO: Figure out how to make this typing exact. This currently relies on
|
|
127
|
+
// the generic typing being sound enough to know that the message can only
|
|
128
|
+
// contain a single static text node and no placeholders or rich text.
|
|
129
|
+
return resolved.ast[0].value;
|
|
130
|
+
};
|
|
131
|
+
/**
|
|
132
|
+
* Return a raw string representing the syntax of the original message, as
|
|
133
|
+
* authored, with no values replaced. The result of this function could be
|
|
134
|
+
* written back to the definition file for the message and re-parsed to
|
|
135
|
+
* create an identical message to the original.
|
|
136
|
+
*
|
|
137
|
+
* This should rarely be necessary outside of sending raw messages to other
|
|
138
|
+
* applications that do their own message parsing.
|
|
139
|
+
*/
|
|
140
|
+
IntlManager.prototype.reserialize = function (message) {
|
|
141
|
+
var resolved = message(this.currentLocale);
|
|
142
|
+
if (typeof resolved === 'string')
|
|
143
|
+
return resolved;
|
|
144
|
+
return resolved.reserialize();
|
|
128
145
|
};
|
|
129
146
|
IntlManager.prototype.bindFormatValues = function (Builder, message, values) {
|
|
130
147
|
return (0, format_1.bindFormatValues)(Builder, message.ast, [this.currentLocale, this.defaultLocale], this.intl.formatters, this.intl.formats, values);
|
package/dist/message.d.ts
CHANGED
|
@@ -3,4 +3,9 @@ export declare class InternalIntlMessage {
|
|
|
3
3
|
locale: string;
|
|
4
4
|
ast: FormatJsNode[];
|
|
5
5
|
constructor(messageOrAst: FormatJsNode[], locale: string);
|
|
6
|
+
/**
|
|
7
|
+
* Return a stringified serialization of this message's AST, with no
|
|
8
|
+
* formatting or values applied.
|
|
9
|
+
*/
|
|
10
|
+
reserialize(): string;
|
|
6
11
|
}
|
package/dist/message.js
CHANGED
|
@@ -1,11 +1,186 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __values = (this && this.__values) || function(o) {
|
|
3
|
+
var s = typeof Symbol === "function" && Symbol.iterator, m = s && o[s], i = 0;
|
|
4
|
+
if (m) return m.call(o);
|
|
5
|
+
if (o && typeof o.length === "number") return {
|
|
6
|
+
next: function () {
|
|
7
|
+
if (o && i >= o.length) o = void 0;
|
|
8
|
+
return { value: o && o[i++], done: !o };
|
|
9
|
+
}
|
|
10
|
+
};
|
|
11
|
+
throw new TypeError(s ? "Object is not iterable." : "Symbol.iterator is not defined.");
|
|
12
|
+
};
|
|
13
|
+
var __read = (this && this.__read) || function (o, n) {
|
|
14
|
+
var m = typeof Symbol === "function" && o[Symbol.iterator];
|
|
15
|
+
if (!m) return o;
|
|
16
|
+
var i = m.call(o), r, ar = [], e;
|
|
17
|
+
try {
|
|
18
|
+
while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value);
|
|
19
|
+
}
|
|
20
|
+
catch (error) { e = { error: error }; }
|
|
21
|
+
finally {
|
|
22
|
+
try {
|
|
23
|
+
if (r && !r.done && (m = i["return"])) m.call(i);
|
|
24
|
+
}
|
|
25
|
+
finally { if (e) throw e.error; }
|
|
26
|
+
}
|
|
27
|
+
return ar;
|
|
28
|
+
};
|
|
2
29
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
30
|
exports.InternalIntlMessage = void 0;
|
|
31
|
+
var keyless_json_1 = require("./keyless-json");
|
|
4
32
|
var InternalIntlMessage = /** @class */ (function () {
|
|
5
33
|
function InternalIntlMessage(messageOrAst, locale) {
|
|
6
34
|
this.locale = locale;
|
|
7
35
|
this.ast = messageOrAst;
|
|
8
36
|
}
|
|
37
|
+
/**
|
|
38
|
+
* Return a stringified serialization of this message's AST, with no
|
|
39
|
+
* formatting or values applied.
|
|
40
|
+
*/
|
|
41
|
+
InternalIntlMessage.prototype.reserialize = function () {
|
|
42
|
+
var result = { value: '' };
|
|
43
|
+
serializeFormatJsAst(this.ast, result);
|
|
44
|
+
return result.value;
|
|
45
|
+
};
|
|
9
46
|
return InternalIntlMessage;
|
|
10
47
|
}());
|
|
11
48
|
exports.InternalIntlMessage = InternalIntlMessage;
|
|
49
|
+
// Accepting an object as the `result` parameter lets the same string get passed around and
|
|
50
|
+
// appended, rather than creating a bunch of intermediate strings.
|
|
51
|
+
function serializeFormatJsAst(ast, result) {
|
|
52
|
+
var e_1, _a, e_2, _b, e_3, _c;
|
|
53
|
+
try {
|
|
54
|
+
for (var ast_1 = __values(ast), ast_1_1 = ast_1.next(); !ast_1_1.done; ast_1_1 = ast_1.next()) {
|
|
55
|
+
var node = ast_1_1.value;
|
|
56
|
+
switch (node.type) {
|
|
57
|
+
case keyless_json_1.FormatJsNodeType.Literal:
|
|
58
|
+
result.value += node.value;
|
|
59
|
+
return;
|
|
60
|
+
case keyless_json_1.FormatJsNodeType.Argument:
|
|
61
|
+
// Empties are an artifact of our parsing strategy, not necessary here.
|
|
62
|
+
if (result.value === '$_')
|
|
63
|
+
return;
|
|
64
|
+
result.value += '{' + node.value + '}';
|
|
65
|
+
return;
|
|
66
|
+
case keyless_json_1.FormatJsNodeType.Date:
|
|
67
|
+
result.value += '{' + node.value + ', date';
|
|
68
|
+
if (node.style != null) {
|
|
69
|
+
result.value += ', ' + node.style;
|
|
70
|
+
}
|
|
71
|
+
result.value += '}';
|
|
72
|
+
return;
|
|
73
|
+
case keyless_json_1.FormatJsNodeType.Time:
|
|
74
|
+
result.value += '{' + node.value + ', time';
|
|
75
|
+
if (node.style != null) {
|
|
76
|
+
result.value += ', ' + node.style;
|
|
77
|
+
}
|
|
78
|
+
result.value += '}';
|
|
79
|
+
return;
|
|
80
|
+
case keyless_json_1.FormatJsNodeType.Number:
|
|
81
|
+
result.value += '{' + node.value + ', number';
|
|
82
|
+
if (node.style != null) {
|
|
83
|
+
result.value += ', ' + node.style;
|
|
84
|
+
}
|
|
85
|
+
result.value += '}';
|
|
86
|
+
return;
|
|
87
|
+
case keyless_json_1.FormatJsNodeType.Plural: {
|
|
88
|
+
var pluralType = node.pluralType == 'ordinal' ? 'selectordinal' : 'plural';
|
|
89
|
+
result.value += '{' + node.value + ', ' + pluralType + ', ';
|
|
90
|
+
if (node.offset) {
|
|
91
|
+
result.value += 'offset:' + node.offset;
|
|
92
|
+
}
|
|
93
|
+
try {
|
|
94
|
+
for (var _d = (e_2 = void 0, __values(Object.entries(node.options))), _e = _d.next(); !_e.done; _e = _d.next()) {
|
|
95
|
+
var _f = __read(_e.value, 2), name = _f[0], arm = _f[1];
|
|
96
|
+
result.value += ' ' + name + ' {';
|
|
97
|
+
serializeFormatJsAst(arm.value, result);
|
|
98
|
+
result.value += '}';
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
catch (e_2_1) { e_2 = { error: e_2_1 }; }
|
|
102
|
+
finally {
|
|
103
|
+
try {
|
|
104
|
+
if (_e && !_e.done && (_b = _d.return)) _b.call(_d);
|
|
105
|
+
}
|
|
106
|
+
finally { if (e_2) throw e_2.error; }
|
|
107
|
+
}
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
case keyless_json_1.FormatJsNodeType.Pound:
|
|
111
|
+
result.value += '#';
|
|
112
|
+
return;
|
|
113
|
+
case keyless_json_1.FormatJsNodeType.Select: {
|
|
114
|
+
result.value += '{' + node.value + ', select, ';
|
|
115
|
+
try {
|
|
116
|
+
for (var _g = (e_3 = void 0, __values(Object.entries(node.options))), _h = _g.next(); !_h.done; _h = _g.next()) {
|
|
117
|
+
var _j = __read(_h.value, 2), name = _j[0], arm = _j[1];
|
|
118
|
+
result.value += ' ' + name + ' {';
|
|
119
|
+
serializeFormatJsAst(arm.value, result);
|
|
120
|
+
result.value += '}';
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
catch (e_3_1) { e_3 = { error: e_3_1 }; }
|
|
124
|
+
finally {
|
|
125
|
+
try {
|
|
126
|
+
if (_h && !_h.done && (_c = _g.return)) _c.call(_g);
|
|
127
|
+
}
|
|
128
|
+
finally { if (e_3) throw e_3.error; }
|
|
129
|
+
}
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
case keyless_json_1.FormatJsNodeType.Tag:
|
|
133
|
+
serializeFormatJsTag(node, result);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
catch (e_1_1) { e_1 = { error: e_1_1 }; }
|
|
139
|
+
finally {
|
|
140
|
+
try {
|
|
141
|
+
if (ast_1_1 && !ast_1_1.done && (_a = ast_1.return)) _a.call(ast_1);
|
|
142
|
+
}
|
|
143
|
+
finally { if (e_1) throw e_1.error; }
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
function serializeFormatJsTag(node, result) {
|
|
147
|
+
switch (node.value) {
|
|
148
|
+
case '$b':
|
|
149
|
+
result.value += '**';
|
|
150
|
+
serializeFormatJsAst(node.children, result);
|
|
151
|
+
result.value += '**';
|
|
152
|
+
return;
|
|
153
|
+
case '$i':
|
|
154
|
+
result.value += '*';
|
|
155
|
+
serializeFormatJsAst(node.children, result);
|
|
156
|
+
result.value += '*';
|
|
157
|
+
return;
|
|
158
|
+
case '$code':
|
|
159
|
+
result.value += '`';
|
|
160
|
+
serializeFormatJsAst(node.children, result);
|
|
161
|
+
result.value += '`';
|
|
162
|
+
return;
|
|
163
|
+
case '$p':
|
|
164
|
+
serializeFormatJsAst(node.children, result);
|
|
165
|
+
result.value += '\n\n';
|
|
166
|
+
return;
|
|
167
|
+
case '$link':
|
|
168
|
+
// The target is the first child of the link. We don't have to care if it's a placeholder
|
|
169
|
+
// or not, because the serialization will automatically remove the extra empty.
|
|
170
|
+
var _a = __read(node.children), target = _a[0], children = _a.slice(1);
|
|
171
|
+
result.value += '[';
|
|
172
|
+
serializeFormatJsAst(children, result);
|
|
173
|
+
result.value += '](';
|
|
174
|
+
if (target != null) {
|
|
175
|
+
serializeFormatJsAst([target], result);
|
|
176
|
+
}
|
|
177
|
+
result.value += ')';
|
|
178
|
+
return;
|
|
179
|
+
default:
|
|
180
|
+
// Any other tag name is a hook, which just adds the `$[` on a link tag.
|
|
181
|
+
result.value += '$[';
|
|
182
|
+
serializeFormatJsAst(node.children, result);
|
|
183
|
+
result.value += '](' + node.value + ')';
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
}
|
package/dist/types.d.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@discord/intl",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2-canary.96b50b6",
|
|
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,9 @@
|
|
|
15
15
|
"src"
|
|
16
16
|
],
|
|
17
17
|
"dependencies": {
|
|
18
|
+
"@formatjs/icu-skeleton-parser": "1.8.2",
|
|
18
19
|
"@formatjs/intl": "^2.10.1",
|
|
20
|
+
"@intrnl/xxhash64": "^0.1.2",
|
|
19
21
|
"intl-messageformat": "^10.5.11"
|
|
20
22
|
},
|
|
21
23
|
"devDependencies": {
|
|
@@ -25,8 +27,8 @@
|
|
|
25
27
|
},
|
|
26
28
|
"peerDependencies": {
|
|
27
29
|
"@rspack/core": "0.x || 1.x",
|
|
28
|
-
"
|
|
29
|
-
"
|
|
30
|
+
"react": "> 16",
|
|
31
|
+
"webpack": "^5.0.0"
|
|
30
32
|
},
|
|
31
33
|
"peerDependenciesMeta": {
|
|
32
34
|
"@rspack/core": {
|
package/src/format.ts
CHANGED
|
@@ -20,6 +20,11 @@
|
|
|
20
20
|
import { FormatJsNode, FormatJsNodeType } from './keyless-json';
|
|
21
21
|
import { Formats, Formatters, MissingValueError } from 'intl-messageformat';
|
|
22
22
|
import { RichTextTagNames } from './types';
|
|
23
|
+
import {
|
|
24
|
+
parseDateTimeSkeleton,
|
|
25
|
+
parseNumberSkeleton,
|
|
26
|
+
parseNumberSkeletonFromString,
|
|
27
|
+
} from '@formatjs/icu-skeleton-parser';
|
|
23
28
|
|
|
24
29
|
/**
|
|
25
30
|
* Returns true if the tag name should be considered a rich text tag that
|
|
@@ -96,10 +101,12 @@ export function bindFormatValuesWithBuilder<T, Builder extends FormatBuilder<T>>
|
|
|
96
101
|
case FormatJsNodeType.Date: {
|
|
97
102
|
// Distinct from FormatJS: We don't currently parse the skeleton ahead of time in the AST,
|
|
98
103
|
// so this manages parsing the skeleton as well before passing it onto the date formatter.
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
104
|
+
const style =
|
|
105
|
+
node.style in formats.date
|
|
106
|
+
? formats.date[node.style]
|
|
107
|
+
: node.style != null
|
|
108
|
+
? parseDateTimeSkeleton(node.style)
|
|
109
|
+
: undefined;
|
|
103
110
|
// @ts-expect-error Cast string values to dates properly.
|
|
104
111
|
builder.pushLiteralText(formatters.getDateTimeFormat(locales, style).format(value));
|
|
105
112
|
break;
|
|
@@ -107,10 +114,12 @@ export function bindFormatValuesWithBuilder<T, Builder extends FormatBuilder<T>>
|
|
|
107
114
|
case FormatJsNodeType.Time: {
|
|
108
115
|
// Distinct from FormatJS: We don't currently parse the skeleton ahead of time in the AST,
|
|
109
116
|
// so this manages parsing the skeleton as well before passing it onto the date formatter.
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
117
|
+
const style =
|
|
118
|
+
node.style in formats.time
|
|
119
|
+
? formats.time[node.style]
|
|
120
|
+
: node.style != null
|
|
121
|
+
? parseDateTimeSkeleton(node.style)
|
|
122
|
+
: undefined; // TODO: parseSkeleton();
|
|
114
123
|
builder.pushLiteralText(
|
|
115
124
|
// @ts-expect-error Cast string values to dates properly.
|
|
116
125
|
formatters.getDateTimeFormat(locales, style).format(value),
|
|
@@ -120,14 +129,15 @@ export function bindFormatValuesWithBuilder<T, Builder extends FormatBuilder<T>>
|
|
|
120
129
|
case FormatJsNodeType.Number: {
|
|
121
130
|
// Distinct from FormatJS: We don't currently parse the skeleton ahead of time in the AST,
|
|
122
131
|
// so this manages parsing the skeleton as well before passing it onto the date formatter.
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
const scaledValue =
|
|
130
|
-
|
|
132
|
+
const style =
|
|
133
|
+
node.style in formats.number
|
|
134
|
+
? formats.number[node.style]
|
|
135
|
+
: node.style != null
|
|
136
|
+
? parseNumberSkeleton(parseNumberSkeletonFromString(node.style))
|
|
137
|
+
: undefined;
|
|
138
|
+
const scaledValue =
|
|
139
|
+
// @ts-expect-error This is a weird cast that's not accurate, but works in the short term.
|
|
140
|
+
typeof value !== 'number' ? (value as number) : ((value as number) * style?.scale ?? 1);
|
|
131
141
|
builder.pushLiteralText(formatters.getNumberFormat(locales, style).format(scaledValue));
|
|
132
142
|
break;
|
|
133
143
|
}
|
package/src/formatters/ast.ts
CHANGED
|
@@ -20,8 +20,9 @@ export type AstFunctionTypes = FunctionTypes<RichTextNode, object>;
|
|
|
20
20
|
export enum RichTextNodeType {
|
|
21
21
|
Text = 'text',
|
|
22
22
|
Strong = 'strong',
|
|
23
|
-
Emphasis = '
|
|
24
|
-
|
|
23
|
+
Emphasis = 'em',
|
|
24
|
+
Strikethrough = 's',
|
|
25
|
+
Code = 'inlineCode',
|
|
25
26
|
Link = 'link',
|
|
26
27
|
Paragraph = 'paragraph',
|
|
27
28
|
/**
|
|
@@ -49,6 +50,9 @@ interface RichTextStrongNode extends RichTextNodeBase<RichTextNode[]> {
|
|
|
49
50
|
interface RichTextEmphasisNode extends RichTextNodeBase<RichTextNode[]> {
|
|
50
51
|
type: RichTextNodeType.Emphasis;
|
|
51
52
|
}
|
|
53
|
+
interface RichTextStrikethroughNode extends RichTextNodeBase<RichTextNode[]> {
|
|
54
|
+
type: RichTextNodeType.Strikethrough;
|
|
55
|
+
}
|
|
52
56
|
interface RichTextCodeNode extends RichTextNodeBase<RichTextNode[]> {
|
|
53
57
|
type: RichTextNodeType.Code;
|
|
54
58
|
}
|
|
@@ -67,6 +71,7 @@ export type RichTextNode =
|
|
|
67
71
|
| RichTextTextNode
|
|
68
72
|
| RichTextStrongNode
|
|
69
73
|
| RichTextEmphasisNode
|
|
74
|
+
| RichTextStrikethroughNode
|
|
70
75
|
| RichTextCodeNode
|
|
71
76
|
| RichTextParagraphNode
|
|
72
77
|
| RichTextLinkNode
|
|
@@ -75,8 +80,9 @@ export type RichTextNode =
|
|
|
75
80
|
const AST_RICH_TEXT_ELEMENTS: RichTextFormattingMap<AstFunctionTypes['hook']> = {
|
|
76
81
|
$_: () => ({ type: RichTextNodeType.Text, content: '' }),
|
|
77
82
|
$b: (content) => ({ type: RichTextNodeType.Strong, content }),
|
|
78
|
-
$code: (content) => ({ type: RichTextNodeType.Code, content }),
|
|
79
83
|
$i: (content) => ({ type: RichTextNodeType.Emphasis, content }),
|
|
84
|
+
$del: (content) => ({ type: RichTextNodeType.Strikethrough, content }),
|
|
85
|
+
$code: (content) => ({ type: RichTextNodeType.Code, content }),
|
|
80
86
|
$link: ([target, ...content]) => ({
|
|
81
87
|
type: RichTextNodeType.Link,
|
|
82
88
|
target: target.content as string,
|
|
@@ -102,7 +108,7 @@ class AstBuilder extends FormatBuilder<RichTextNode> {
|
|
|
102
108
|
|
|
103
109
|
pushLiteralText(text: string) {
|
|
104
110
|
const last = this.result[this.result.length - 1];
|
|
105
|
-
if (last.type === RichTextNodeType.Text) {
|
|
111
|
+
if (last != null && last.type === RichTextNodeType.Text) {
|
|
106
112
|
last.content += text;
|
|
107
113
|
} else {
|
|
108
114
|
this.result.push({ type: RichTextNodeType.Text, content: text });
|
package/src/formatters/index.ts
CHANGED
|
@@ -21,8 +21,9 @@ export type MarkdownFunctionTypes = FunctionTypes<string>;
|
|
|
21
21
|
const MARKDOWN_RICH_TEXT_ELEMENTS: RichTextFormattingMap<MarkdownFunctionTypes['hook']> = {
|
|
22
22
|
$_: () => '',
|
|
23
23
|
$b: (content) => '**' + content.join('') + '**',
|
|
24
|
-
$code: (content) => '`' + content.join('') + '`',
|
|
25
24
|
$i: (content) => '*' + content.join('') + '*',
|
|
25
|
+
$del: (content) => '~~' + content.join('') + '~~',
|
|
26
|
+
$code: (content) => '`' + content.join('') + '`',
|
|
26
27
|
$link: ([target, ...content]) => '[' + content.join('') + '](' + target + ')',
|
|
27
28
|
$p: (content) => content.join('') + '\n\n',
|
|
28
29
|
};
|
package/src/formatters/react.ts
CHANGED
|
@@ -29,8 +29,9 @@ const h = React.createElement;
|
|
|
29
29
|
export const DEFAULT_REACT_RICH_TEXT_ELEMENTS: RichTextFormattingMap<ReactFunctionTypes['hook']> = {
|
|
30
30
|
$_: () => '',
|
|
31
31
|
$b: (content, key) => h('strong', { key }, content),
|
|
32
|
-
$code: (content, key) => h('code', { key }, content),
|
|
33
32
|
$i: (content, key) => h('em', { key }, content),
|
|
33
|
+
$del: (content, key) => h('del', { key }, content),
|
|
34
|
+
$code: (content, key) => h('code', { key }, content),
|
|
34
35
|
// $link will always be [href, <empty>, ...content]
|
|
35
36
|
$link: ([href, ...content], key) => h('a', { href, key }, content),
|
|
36
37
|
$p: (content, key) => h('p', { key }, content),
|
|
@@ -70,10 +71,9 @@ function createReactBuilder(richTextElements: RichTextFormattingMap<ReactFunctio
|
|
|
70
71
|
};
|
|
71
72
|
}
|
|
72
73
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
>;
|
|
74
|
+
// This is explicitly `ReactElement | string` and _not_ `ReactNode`, because nullish values and any
|
|
75
|
+
// other type that ReactNode accepts (boolean, number, etc.) are _not_ valid children of a message.
|
|
76
|
+
export type ReactIntlMessage = Array<React.ReactElement | string> & { __brand: 'discord-intl' };
|
|
77
77
|
|
|
78
78
|
export function formatReact(
|
|
79
79
|
this: IntlManager,
|
|
@@ -82,11 +82,11 @@ export function formatReact(
|
|
|
82
82
|
Builder: FormatBuilderConstructor<React.ReactElement>,
|
|
83
83
|
): ReactIntlMessage {
|
|
84
84
|
if (typeof message === 'string') {
|
|
85
|
-
return
|
|
85
|
+
return [message] as ReactIntlMessage;
|
|
86
86
|
}
|
|
87
87
|
|
|
88
88
|
const parts = this.bindFormatValues(Builder, message, values);
|
|
89
|
-
return
|
|
89
|
+
return parts as ReactIntlMessage;
|
|
90
90
|
}
|
|
91
91
|
|
|
92
92
|
/**
|
package/src/formatters/string.ts
CHANGED
|
@@ -34,7 +34,7 @@ export class StringBuilder extends FormatBuilder<string> {
|
|
|
34
34
|
pushObject(value: object) {
|
|
35
35
|
// Objects are only included in the result if they specify a toString value directly.
|
|
36
36
|
// Otherwise, they would be rendered as `[object Object]`, which is never helpful.
|
|
37
|
-
if ('toString' in value) {
|
|
37
|
+
if (value != null && 'toString' in value) {
|
|
38
38
|
this.result += value.toString();
|
|
39
39
|
}
|
|
40
40
|
}
|
package/src/hash.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { hash as h64 } from '@intrnl/xxhash64';
|
|
2
|
+
|
|
3
|
+
const BASE64_TABLE = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'.split('');
|
|
4
|
+
const IS_BIG_ENDIAN = (() => {
|
|
5
|
+
const array = new Uint8Array(4);
|
|
6
|
+
const view = new Uint32Array(array.buffer);
|
|
7
|
+
return !((view[0] = 1) & array[0]);
|
|
8
|
+
})();
|
|
9
|
+
|
|
10
|
+
function numberToBytes(number) {
|
|
11
|
+
number = BigInt(number);
|
|
12
|
+
const array = [];
|
|
13
|
+
const byteCount = Math.ceil(Math.floor(Math.log2(Number(number)) + 1) / 8);
|
|
14
|
+
for (let i = 0; i < byteCount; i++) {
|
|
15
|
+
array.unshift(Number((number >> BigInt(8 * i)) & BigInt(255)));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const bytes = new Uint8Array(array);
|
|
19
|
+
// The native `hashToMessageKey` always works in Big/Network Endian bytes, so this array
|
|
20
|
+
// needs to be converted to the same endianness to get the same base64 result.
|
|
21
|
+
return IS_BIG_ENDIAN ? bytes : bytes.reverse();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Returns a consistent, short hash of the given key by first processing it through a hash digest,
|
|
26
|
+
* then encoding the first few bytes to base64.
|
|
27
|
+
*
|
|
28
|
+
* This function is specifically written to mirror the native backend hashing function used by
|
|
29
|
+
* `@discord/intl-loader-core`, to be able to hash names at runtime.
|
|
30
|
+
*/
|
|
31
|
+
export function runtimeHashMessageKey(key: string): string {
|
|
32
|
+
const hash = h64(key, 0);
|
|
33
|
+
const bytes = numberToBytes(hash);
|
|
34
|
+
return [
|
|
35
|
+
BASE64_TABLE[bytes[0] >> 2],
|
|
36
|
+
BASE64_TABLE[((bytes[0] & 0x03) << 4) | (bytes[1] >> 4)],
|
|
37
|
+
BASE64_TABLE[((bytes[1] & 0x0f) << 2) | (bytes[2] >> 6)],
|
|
38
|
+
BASE64_TABLE[bytes[2] & 0x3f],
|
|
39
|
+
BASE64_TABLE[bytes[3] >> 2],
|
|
40
|
+
BASE64_TABLE[((bytes[3] & 0x03) << 4) | (bytes[3] >> 4)],
|
|
41
|
+
].join('');
|
|
42
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -2,6 +2,8 @@ import * as React from 'react';
|
|
|
2
2
|
import { RichTextNode } from './formatters';
|
|
3
3
|
|
|
4
4
|
export * from './formatters';
|
|
5
|
+
export { FormatBuilder, FormatBuilderConstructor, bindFormatValues } from './format';
|
|
6
|
+
export { runtimeHashMessageKey } from './hash';
|
|
5
7
|
export { IntlManager, DEFAULT_LOCALE, type FormatFunction } from './intl-manager';
|
|
6
8
|
export {
|
|
7
9
|
createLoader,
|
|
@@ -27,7 +29,7 @@ export type * from './types.d.ts';
|
|
|
27
29
|
* user-generated string).
|
|
28
30
|
*/
|
|
29
31
|
// TODO: Replace this with ReactIntlMessage from `formatters/react`.
|
|
30
|
-
export type ReactIntlMessage = string | React.ReactElement
|
|
32
|
+
export type ReactIntlMessage = string | React.ReactElement | Array<string | React.ReactElement>;
|
|
31
33
|
|
|
32
34
|
/**
|
|
33
35
|
* The return value of `formatToParts` from `@discord/intl`, this type
|
package/src/intl-manager.ts
CHANGED
|
@@ -9,6 +9,7 @@ import type {
|
|
|
9
9
|
} from './types';
|
|
10
10
|
import { InternalIntlMessage } from './message';
|
|
11
11
|
import { bindFormatValues, FormatBuilderConstructor } from './format';
|
|
12
|
+
import { FormatJsLiteral } from './keyless-json';
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
15
|
* Fallback locale used for all internationalization when an operation in the
|
|
@@ -113,11 +114,28 @@ export class IntlManager {
|
|
|
113
114
|
* immediately return the plain string value of the message in the current locale.
|
|
114
115
|
*/
|
|
115
116
|
string<T extends TypedIntlMessageGetter<undefined>>(message: T): string {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
//
|
|
120
|
-
|
|
117
|
+
const resolved = message(this.currentLocale);
|
|
118
|
+
if (resolved == null || resolved.ast.length === 0) return '';
|
|
119
|
+
|
|
120
|
+
// TODO: Figure out how to make this typing exact. This currently relies on
|
|
121
|
+
// the generic typing being sound enough to know that the message can only
|
|
122
|
+
// contain a single static text node and no placeholders or rich text.
|
|
123
|
+
return (resolved.ast[0] as FormatJsLiteral).value;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Return a raw string representing the syntax of the original message, as
|
|
128
|
+
* authored, with no values replaced. The result of this function could be
|
|
129
|
+
* written back to the definition file for the message and re-parsed to
|
|
130
|
+
* create an identical message to the original.
|
|
131
|
+
*
|
|
132
|
+
* This should rarely be necessary outside of sending raw messages to other
|
|
133
|
+
* applications that do their own message parsing.
|
|
134
|
+
*/
|
|
135
|
+
reserialize<T extends IntlMessageGetter>(message: T): string {
|
|
136
|
+
const resolved = message(this.currentLocale);
|
|
137
|
+
if (typeof resolved === 'string') return resolved;
|
|
138
|
+
return resolved.reserialize();
|
|
121
139
|
}
|
|
122
140
|
|
|
123
141
|
bindFormatValues<T>(
|
package/src/message.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { FormatJsNode } from './keyless-json';
|
|
1
|
+
import { FormatJsNode, FormatJsNodeType, FormatJsTag } from './keyless-json';
|
|
2
2
|
|
|
3
3
|
export class InternalIntlMessage {
|
|
4
4
|
locale: string;
|
|
@@ -8,4 +8,122 @@ export class InternalIntlMessage {
|
|
|
8
8
|
this.locale = locale;
|
|
9
9
|
this.ast = messageOrAst;
|
|
10
10
|
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Return a stringified serialization of this message's AST, with no
|
|
14
|
+
* formatting or values applied.
|
|
15
|
+
*/
|
|
16
|
+
reserialize(): string {
|
|
17
|
+
const result = { value: '' };
|
|
18
|
+
serializeFormatJsAst(this.ast, result);
|
|
19
|
+
return result.value;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
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;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
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);
|
|
119
|
+
}
|
|
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;
|
|
128
|
+
}
|
|
11
129
|
}
|