@discord/intl 0.0.1
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/LICENSE +21 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +9 -0
- package/dist/intl-manager.d.ts +59 -0
- package/dist/intl-manager.js +138 -0
- package/dist/keyless-json.d.ts +61 -0
- package/dist/keyless-json.js +107 -0
- package/dist/message-loader.d.ts +60 -0
- package/dist/message-loader.js +168 -0
- package/dist/message.d.ts +41 -0
- package/dist/message.js +148 -0
- package/dist/react.d.ts +24 -0
- package/dist/react.js +80 -0
- package/dist/types.d.ts +158 -0
- package/package.json +43 -0
- package/src/index.ts +4 -0
- package/src/intl-manager.ts +166 -0
- package/src/keyless-json.ts +154 -0
- package/src/message-loader.ts +156 -0
- package/src/message.ts +110 -0
- package/src/react.ts +59 -0
- package/src/types.d.ts +158 -0
package/dist/message.js
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
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
|
+
};
|
|
29
|
+
var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) {
|
|
30
|
+
if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) {
|
|
31
|
+
if (ar || !(i in from)) {
|
|
32
|
+
if (!ar) ar = Array.prototype.slice.call(from, 0, i);
|
|
33
|
+
ar[i] = from[i];
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return to.concat(ar || Array.prototype.slice.call(from));
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.InternalIntlMessage = void 0;
|
|
40
|
+
var icu_messageformat_parser_1 = require("@formatjs/icu-messageformat-parser");
|
|
41
|
+
var intl_messageformat_1 = require("intl-messageformat");
|
|
42
|
+
var InternalIntlMessage = /** @class */ (function () {
|
|
43
|
+
function InternalIntlMessage(messageOrAst, locale) {
|
|
44
|
+
this.locale = locale;
|
|
45
|
+
if (typeof messageOrAst === 'string') {
|
|
46
|
+
// TODO: This part should pre-parse our Markdown shorthand, too, so that
|
|
47
|
+
// the AST that comes out of this is a standardized format, and we don't
|
|
48
|
+
// have to do any special handling after calling `format`. That also
|
|
49
|
+
// removes the need for handling "unsafe" values, since they'll be
|
|
50
|
+
// parsed before values get interpolated. We can actually do that in the
|
|
51
|
+
// loader, too, and avoid any runtime cost for it.
|
|
52
|
+
this.message = messageOrAst;
|
|
53
|
+
this.ast = (0, icu_messageformat_parser_1.parse)(messageOrAst);
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
this.ast = messageOrAst;
|
|
57
|
+
}
|
|
58
|
+
this.plainAst = undefined;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Format this message with the given locale into an AST of chunks that can
|
|
62
|
+
* then be used or rendered into strings, React elements, or anything else as
|
|
63
|
+
* needed.
|
|
64
|
+
*/
|
|
65
|
+
InternalIntlMessage.prototype.formatToParts = function (formatters, formats, values) {
|
|
66
|
+
return (0, intl_messageformat_1.formatToParts)(this.ast, this.locale, formatters, formats, values);
|
|
67
|
+
};
|
|
68
|
+
/**
|
|
69
|
+
* Formats this message just like `formatToParts`, but strips all stylistic
|
|
70
|
+
* elements and tags and returns a plain string with only the literal text of
|
|
71
|
+
* the message remaining.
|
|
72
|
+
*
|
|
73
|
+
* This method works by quickly scanning the string and removing any tag
|
|
74
|
+
* elements, formatting, then concatenating the pieces into a single result.
|
|
75
|
+
*
|
|
76
|
+
* Note that this function does _not_ handle serializing values other than
|
|
77
|
+
* plain/primitive values into strings. All values are simply converted using
|
|
78
|
+
* the `String()` constructor. So, for example, passing a function as a value
|
|
79
|
+
* will directly serialize the function definition into a string, rather than
|
|
80
|
+
* calling the function or evaluating the result.
|
|
81
|
+
*/
|
|
82
|
+
InternalIntlMessage.prototype.formatToPlainString = function (formatters, formats, values) {
|
|
83
|
+
var e_1, _a, _b, e_2, _c;
|
|
84
|
+
// Lazily create the reduced AST if it hasn't already been done.
|
|
85
|
+
if (this.plainAst == null) {
|
|
86
|
+
this.plainAst = [];
|
|
87
|
+
try {
|
|
88
|
+
for (var _d = __values(this.ast), _e = _d.next(); !_e.done; _e = _d.next()) {
|
|
89
|
+
var part = _e.value;
|
|
90
|
+
(_b = this.plainAst).push.apply(_b, __spreadArray([], __read(this._removeRichTags(part)), false));
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
catch (e_1_1) { e_1 = { error: e_1_1 }; }
|
|
94
|
+
finally {
|
|
95
|
+
try {
|
|
96
|
+
if (_e && !_e.done && (_a = _d.return)) _a.call(_d);
|
|
97
|
+
}
|
|
98
|
+
finally { if (e_1) throw e_1.error; }
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
var parts = (0, intl_messageformat_1.formatToParts)(this.plainAst, this.locale, formatters, formats, values);
|
|
102
|
+
var result = '';
|
|
103
|
+
try {
|
|
104
|
+
for (var parts_1 = __values(parts), parts_1_1 = parts_1.next(); !parts_1_1.done; parts_1_1 = parts_1.next()) {
|
|
105
|
+
var part = parts_1_1.value;
|
|
106
|
+
result += String(part.value);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
catch (e_2_1) { e_2 = { error: e_2_1 }; }
|
|
110
|
+
finally {
|
|
111
|
+
try {
|
|
112
|
+
if (parts_1_1 && !parts_1_1.done && (_c = parts_1.return)) _c.call(parts_1);
|
|
113
|
+
}
|
|
114
|
+
finally { if (e_2) throw e_2.error; }
|
|
115
|
+
}
|
|
116
|
+
return result;
|
|
117
|
+
};
|
|
118
|
+
/**
|
|
119
|
+
* Returns the same element with any formatting tags removed. If the element
|
|
120
|
+
* itself is a tag, the children will be hoisted and returned, otherwise the
|
|
121
|
+
* element is returned as an array of itself.
|
|
122
|
+
*/
|
|
123
|
+
InternalIntlMessage.prototype._removeRichTags = function (element) {
|
|
124
|
+
var e_3, _a;
|
|
125
|
+
if (element.type === icu_messageformat_parser_1.TYPE.tag) {
|
|
126
|
+
var childContent = [];
|
|
127
|
+
try {
|
|
128
|
+
for (var _b = __values(element.children), _c = _b.next(); !_c.done; _c = _b.next()) {
|
|
129
|
+
var child = _c.value;
|
|
130
|
+
childContent.push.apply(childContent, __spreadArray([], __read(this._removeRichTags(child)), false));
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
catch (e_3_1) { e_3 = { error: e_3_1 }; }
|
|
134
|
+
finally {
|
|
135
|
+
try {
|
|
136
|
+
if (_c && !_c.done && (_a = _b.return)) _a.call(_b);
|
|
137
|
+
}
|
|
138
|
+
finally { if (e_3) throw e_3.error; }
|
|
139
|
+
}
|
|
140
|
+
return childContent;
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
return [element];
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
return InternalIntlMessage;
|
|
147
|
+
}());
|
|
148
|
+
exports.InternalIntlMessage = InternalIntlMessage;
|
package/dist/react.d.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { IntlManager } from './intl-manager';
|
|
3
|
+
import type { FormatValuesFor, RichTextElementMap, TypedIntlMessageGetter } from './types';
|
|
4
|
+
export declare class IntlManagerReact<DefaultElements extends RichTextElementMap, DefaultValues extends keyof DefaultElements> extends IntlManager<DefaultElements, DefaultValues> {
|
|
5
|
+
/**
|
|
6
|
+
* A reactive component form of `intl.format` that automatically updates when
|
|
7
|
+
* the application's locale changes and when new data is loaded for the
|
|
8
|
+
* subject message.
|
|
9
|
+
*/
|
|
10
|
+
IntlMessage: <T extends TypedIntlMessageGetter<object | undefined>>(props: {
|
|
11
|
+
message: T;
|
|
12
|
+
values?: Omit<FormatValuesFor<T>, DefaultValues>;
|
|
13
|
+
}) => React.FunctionComponentElement<{
|
|
14
|
+
children?: React.ReactNode | undefined;
|
|
15
|
+
}>;
|
|
16
|
+
/**
|
|
17
|
+
* Format the given message as a React component, allowing it to listen for
|
|
18
|
+
* and respond to updates about the current locale and other relevant
|
|
19
|
+
* information.
|
|
20
|
+
*/
|
|
21
|
+
format<T extends TypedIntlMessageGetter<object | undefined>>(message: T): JSX.Element;
|
|
22
|
+
format<T extends TypedIntlMessageGetter<object | undefined>>(message: T, values: Omit<FormatValuesFor<T>, DefaultValues>): JSX.Element;
|
|
23
|
+
format(message: any): JSX.Element;
|
|
24
|
+
}
|
package/dist/react.js
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __extends = (this && this.__extends) || (function () {
|
|
3
|
+
var extendStatics = function (d, b) {
|
|
4
|
+
extendStatics = Object.setPrototypeOf ||
|
|
5
|
+
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
|
|
6
|
+
function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; };
|
|
7
|
+
return extendStatics(d, b);
|
|
8
|
+
};
|
|
9
|
+
return function (d, b) {
|
|
10
|
+
if (typeof b !== "function" && b !== null)
|
|
11
|
+
throw new TypeError("Class extends value " + String(b) + " is not a constructor or null");
|
|
12
|
+
extendStatics(d, b);
|
|
13
|
+
function __() { this.constructor = d; }
|
|
14
|
+
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
|
|
15
|
+
};
|
|
16
|
+
})();
|
|
17
|
+
var __read = (this && this.__read) || function (o, n) {
|
|
18
|
+
var m = typeof Symbol === "function" && o[Symbol.iterator];
|
|
19
|
+
if (!m) return o;
|
|
20
|
+
var i = m.call(o), r, ar = [], e;
|
|
21
|
+
try {
|
|
22
|
+
while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value);
|
|
23
|
+
}
|
|
24
|
+
catch (error) { e = { error: error }; }
|
|
25
|
+
finally {
|
|
26
|
+
try {
|
|
27
|
+
if (r && !r.done && (m = i["return"])) m.call(i);
|
|
28
|
+
}
|
|
29
|
+
finally { if (e) throw e.error; }
|
|
30
|
+
}
|
|
31
|
+
return ar;
|
|
32
|
+
};
|
|
33
|
+
var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) {
|
|
34
|
+
if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) {
|
|
35
|
+
if (ar || !(i in from)) {
|
|
36
|
+
if (!ar) ar = Array.prototype.slice.call(from, 0, i);
|
|
37
|
+
ar[i] = from[i];
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return to.concat(ar || Array.prototype.slice.call(from));
|
|
41
|
+
};
|
|
42
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
43
|
+
exports.IntlManagerReact = void 0;
|
|
44
|
+
var React = require("react");
|
|
45
|
+
var intl_manager_1 = require("./intl-manager");
|
|
46
|
+
var IntlManagerReact = /** @class */ (function (_super) {
|
|
47
|
+
__extends(IntlManagerReact, _super);
|
|
48
|
+
function IntlManagerReact() {
|
|
49
|
+
var _this = _super.apply(this, __spreadArray([], __read(arguments), false)) || this;
|
|
50
|
+
/**
|
|
51
|
+
* A reactive component form of `intl.format` that automatically updates when
|
|
52
|
+
* the application's locale changes and when new data is loaded for the
|
|
53
|
+
* subject message.
|
|
54
|
+
*/
|
|
55
|
+
_this.IntlMessage = function (props) {
|
|
56
|
+
var message = props.message, values = props.values;
|
|
57
|
+
// Use the locale from this point in the application, which may be
|
|
58
|
+
// different from the global locale.
|
|
59
|
+
var locale = React.useSyncExternalStore(_this.onLocaleChange, function () { return _this.currentLocale; });
|
|
60
|
+
// Source the actual message to render for that locale from its loader.
|
|
61
|
+
// TODO(faulty): This can and should be replaced by a
|
|
62
|
+
// `use(messagesLoadedPromise)` once `use` is shipped to stable.
|
|
63
|
+
React.useSyncExternalStore(message.onChange, function () { return message(locale); });
|
|
64
|
+
// The return the formatted version of that string.
|
|
65
|
+
if (typeof message === 'string')
|
|
66
|
+
return message;
|
|
67
|
+
return React.createElement(React.Fragment, undefined, _this.formatToParts(message, values));
|
|
68
|
+
};
|
|
69
|
+
return _this;
|
|
70
|
+
}
|
|
71
|
+
IntlManagerReact.prototype.format = function (message, values) {
|
|
72
|
+
// A string literal means there's no locale dependency, so it can just be
|
|
73
|
+
// formatted directly without any subscription.
|
|
74
|
+
if (typeof message === 'string')
|
|
75
|
+
return this.formatToParts(message);
|
|
76
|
+
return React.createElement(this.IntlMessage, { message: message, values: values });
|
|
77
|
+
};
|
|
78
|
+
return IntlManagerReact;
|
|
79
|
+
}(intl_manager_1.IntlManager));
|
|
80
|
+
exports.IntlManagerReact = IntlManagerReact;
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import type { InternalIntlMessage } from './message';
|
|
2
|
+
import type { IntlMessageGetterAdditions } from './message-loader';
|
|
3
|
+
/**
|
|
4
|
+
* Use this function to define messages as part of the `@discord/intl`
|
|
5
|
+
* system, with type checking support.
|
|
6
|
+
*
|
|
7
|
+
* This function does not actually exist, and gets compiled out as part
|
|
8
|
+
* of the bundling process both on web and mobile. See the
|
|
9
|
+
* `@discord/intl/README.md` for more information.
|
|
10
|
+
*/
|
|
11
|
+
export declare function defineMessages(messages: object): object;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* A map of tag names to render functions to apply when the tags are
|
|
15
|
+
* encountered while formatting a message. Each `IntlManager` can be
|
|
16
|
+
* constructed with a different map of these elements, for example to allow
|
|
17
|
+
* one manager to render React output while another renders accessibility text.
|
|
18
|
+
*/
|
|
19
|
+
export type RichTextElementMap = Record<string, (chunks: any[]) => any>;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Messages with no rich formatting and no value interpolations are just plain
|
|
23
|
+
* strings, and are able to skip multiple formatting steps to work more
|
|
24
|
+
* efficiently. This type represents those messages distinctly.
|
|
25
|
+
*/
|
|
26
|
+
type PlainIntlMessage = string;
|
|
27
|
+
/**
|
|
28
|
+
* A richly-formatted message that injects dynamic values while formatting,
|
|
29
|
+
* with the types of those values defined by the `FormatValues` object. All
|
|
30
|
+
* messages of this type must have at least one value in the object, otherwise
|
|
31
|
+
* the message should be typed as a `PlainIntlMessage`.
|
|
32
|
+
*/
|
|
33
|
+
type TypedIntlMessage<FormatValues extends object> = InternalIntlMessage & {
|
|
34
|
+
values: FormatValues;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
type AnyIntlMessage<FormatValues extends object | undefined = undefined> =
|
|
38
|
+
FormatValues extends object ? TypedIntlMessage<FormatValues> : PlainIntlMessage;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Getter function that retrieves a message that best matches the requested
|
|
42
|
+
* locale. `FormatValues` represents either an object containing all of the
|
|
43
|
+
* value types that are required to format the message, or `undefined` to
|
|
44
|
+
* represent that there are no values required and the message is a plain
|
|
45
|
+
* string.
|
|
46
|
+
*/
|
|
47
|
+
export type TypedIntlMessageGetter<FormatValues extends object | undefined> =
|
|
48
|
+
IntlMessageGetterAdditions & {
|
|
49
|
+
(locale: string): AnyIntlMessage<FormatValues>;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Common utility type for converting a union type to an intersection of all
|
|
54
|
+
* the types in that union.
|
|
55
|
+
*/
|
|
56
|
+
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (k: infer I) => void
|
|
57
|
+
? I
|
|
58
|
+
: never;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Given a message getter of type `T` (which may be a union of multiple message
|
|
62
|
+
* types), return a combined type representing the intersection of all values
|
|
63
|
+
* needed to satisfy the mix of values.
|
|
64
|
+
*
|
|
65
|
+
* For example, given a variable representing one of two message getters, where
|
|
66
|
+
* one requires a single value to be formatted, but the other requires an
|
|
67
|
+
* additional value on top of that one:
|
|
68
|
+
*
|
|
69
|
+
* ```typescript
|
|
70
|
+
* // message1: "Hello {username}"
|
|
71
|
+
* // message2: "Hello {username}, it's {currentDate, date}"
|
|
72
|
+
* const message = someCondition ? message1 || message2;
|
|
73
|
+
* ```
|
|
74
|
+
*
|
|
75
|
+
* Attempting to format this message would require that both `username` _and_
|
|
76
|
+
* `currentDate` are supplied, so that all possible cases are covered, which is
|
|
77
|
+
* what this type resolves:
|
|
78
|
+
*
|
|
79
|
+
* ```typescript
|
|
80
|
+
* type ResolvedValuesTypes = FormatValuesFor<typeof message>;
|
|
81
|
+
* // => {username: string, currentDate: Date}
|
|
82
|
+
* ```
|
|
83
|
+
*
|
|
84
|
+
* This values type can then be used to enforce that all values are passed to
|
|
85
|
+
* functions that format the message, like `i18n.format`:
|
|
86
|
+
*
|
|
87
|
+
* ```typescript
|
|
88
|
+
* declare function format<T>(message: T, values: FormatValuesFor<T>): string;
|
|
89
|
+
* ```
|
|
90
|
+
*
|
|
91
|
+
* ## Mixed message types
|
|
92
|
+
*
|
|
93
|
+
* If T is a union of message types including both rich messages requiring
|
|
94
|
+
* values _and_ plain string messages with no values, this type will still
|
|
95
|
+
* properly resolve all of the required values, assuming that the getters for
|
|
96
|
+
* the plain messages use the convention of passing `undefined` as the generic
|
|
97
|
+
* argument, representing "no values should be passed for this message".
|
|
98
|
+
*
|
|
99
|
+
*
|
|
100
|
+
* ## Why this type?
|
|
101
|
+
*
|
|
102
|
+
* All "messages" are really defined as functions that return a specific
|
|
103
|
+
* value of the message for a requested locale, `(locale: string) => Message`.
|
|
104
|
+
* This is named `TypedIntlMessageGetter<T>`, where `T` is the definition of
|
|
105
|
+
* the values required to format the message.
|
|
106
|
+
*
|
|
107
|
+
* This type requires being passed the entire getter function rather than the
|
|
108
|
+
* returned message types, as narrowing to the returned type prevents
|
|
109
|
+
* TypeScript from distributing the union properly. This surfaces as error
|
|
110
|
+
* messages showing up on the _message_ type rather than the _value_ type when
|
|
111
|
+
* they don't match, which doesn't give the user any helpful information about
|
|
112
|
+
* what's wrong. This method ensures the value type is always the one being
|
|
113
|
+
* checked against the provided message types, and error messages will be
|
|
114
|
+
* accurate as a result.
|
|
115
|
+
*/
|
|
116
|
+
type FormatValuesFor<T> =
|
|
117
|
+
// `[]` syntax forces TypeScript to not distribute the possible union of
|
|
118
|
+
// types, meaning this represents "_all_ values of T are an extension of
|
|
119
|
+
// the parent type", which in this case represents messages with no
|
|
120
|
+
// format values. If that condition is matched, then `FormatValues` _cannot_
|
|
121
|
+
// be supplied, represented by the `never`. This prevents accidentally
|
|
122
|
+
// passing an object, even an empty one, for those strings, and allows the
|
|
123
|
+
// formatter to optimize a little more on each call.
|
|
124
|
+
[T] extends [TypedIntlMessageGetter<undefined>]
|
|
125
|
+
? never
|
|
126
|
+
: // The `never` conditino needs to be repeated here, but this time _without_
|
|
127
|
+
// the `[]` syntax, so that only the union elements with actual values are
|
|
128
|
+
// included in the union. Without this, the `infer U` on the latter side
|
|
129
|
+
// fails to resolve and the type just becomes `never`.
|
|
130
|
+
UnionToIntersection<
|
|
131
|
+
T extends TypedIntlMessageGetter<undefined>
|
|
132
|
+
? never
|
|
133
|
+
: T extends TypedIntlMessageGetter<infer U>
|
|
134
|
+
? U
|
|
135
|
+
: never
|
|
136
|
+
>;
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* A generic definition of a `format*` function that accepts any message
|
|
140
|
+
* argument as the first parameter, and the required values object for that
|
|
141
|
+
* message as the second parameter. The second parameter
|
|
142
|
+
*
|
|
143
|
+
* An optional third parameter for additional options can be specified as the
|
|
144
|
+
* second generic type parameter.
|
|
145
|
+
*/
|
|
146
|
+
export interface IntlFormatFn<
|
|
147
|
+
T extends object | undefined,
|
|
148
|
+
DefaultValues extends string | number | symbol,
|
|
149
|
+
OptionsType,
|
|
150
|
+
ResultType,
|
|
151
|
+
> {
|
|
152
|
+
(message: TypedIntlMessageGetter<T>, options?: OptionsType): ResultType;
|
|
153
|
+
(
|
|
154
|
+
message: TypedIntlMessageGetter<T>,
|
|
155
|
+
values: Omit<FormatValuesFor<T>, DefaultValues>,
|
|
156
|
+
options?: OptionsType,
|
|
157
|
+
): ResultType;
|
|
158
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@discord/intl",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"description": "Client runtime for managing messages and translations in a React project.",
|
|
6
|
+
"main": "./src/index.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"default": "./dist/index.js",
|
|
10
|
+
"types": "./dist/index.d.ts"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"dist",
|
|
15
|
+
"src"
|
|
16
|
+
],
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"@formatjs/icu-messageformat-parser": "^2.7.6",
|
|
19
|
+
"@formatjs/intl": "^2.10.1",
|
|
20
|
+
"intl-messageformat": "^10.5.11"
|
|
21
|
+
},
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"@swc/cli": "^0.3.12",
|
|
24
|
+
"@swc/core": "^1.4.11",
|
|
25
|
+
"typescript": "*"
|
|
26
|
+
},
|
|
27
|
+
"peerDependencies": {
|
|
28
|
+
"@rspack/core": "0.x || 1.x",
|
|
29
|
+
"webpack": "^5.0.0",
|
|
30
|
+
"react": "> 16"
|
|
31
|
+
},
|
|
32
|
+
"peerDependenciesMeta": {
|
|
33
|
+
"@rspack/core": {
|
|
34
|
+
"optional": true
|
|
35
|
+
},
|
|
36
|
+
"webpack": {
|
|
37
|
+
"optional": true
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
"scripts": {
|
|
41
|
+
"build": "tsc && cp -r ./src/*.d.ts dist/"
|
|
42
|
+
}
|
|
43
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { IntlShape, createIntl } from '@formatjs/intl';
|
|
2
|
+
import {
|
|
3
|
+
PART_TYPE as FormatPartType,
|
|
4
|
+
Formats,
|
|
5
|
+
Formatters,
|
|
6
|
+
IntlMessageFormat,
|
|
7
|
+
} from 'intl-messageformat';
|
|
8
|
+
|
|
9
|
+
import { LocaleImportMap, MessageLoader } from './message-loader';
|
|
10
|
+
|
|
11
|
+
import type { TypedIntlMessageGetter, FormatValuesFor, RichTextElementMap } from './types';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Fallback locale used for all internationalization when an operation in the
|
|
15
|
+
* requested locale is not possible.
|
|
16
|
+
*/
|
|
17
|
+
export const DEFAULT_LOCALE: string = 'en-US';
|
|
18
|
+
|
|
19
|
+
export class IntlManager<
|
|
20
|
+
DefaultElements extends RichTextElementMap,
|
|
21
|
+
DefaultValues extends keyof DefaultElements,
|
|
22
|
+
> {
|
|
23
|
+
defaultLocale: string;
|
|
24
|
+
currentLocale: string;
|
|
25
|
+
intl: IntlShape;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* When formatting tag elements like `<b>value</b>`, these functions will
|
|
29
|
+
* automatically be used by all messages, unless they are explicitly removed
|
|
30
|
+
* for a specific call, either by overriding the value or by calling a
|
|
31
|
+
* dedicated method like `formatToPlainString` which will remove all
|
|
32
|
+
* rich elements from the string and return only the plain text result.
|
|
33
|
+
*/
|
|
34
|
+
defaultRichTextElements: DefaultElements;
|
|
35
|
+
|
|
36
|
+
_localeSubscriptions: Set<(locale: string) => void>;
|
|
37
|
+
|
|
38
|
+
constructor(defaultLocale: string = DEFAULT_LOCALE, defaultRichTextElements: DefaultElements) {
|
|
39
|
+
this.defaultLocale = defaultLocale;
|
|
40
|
+
this.currentLocale = defaultLocale;
|
|
41
|
+
this.intl = createIntl({
|
|
42
|
+
formats: IntlMessageFormat.formats,
|
|
43
|
+
defaultLocale,
|
|
44
|
+
locale: defaultLocale,
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
this.defaultRichTextElements = defaultRichTextElements;
|
|
48
|
+
this._localeSubscriptions = new Set();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Set the locale for this package to use. This is a global change, and all
|
|
53
|
+
* methods on this object will use this locale for formatting.
|
|
54
|
+
*/
|
|
55
|
+
setLocale(locale: string) {
|
|
56
|
+
this.currentLocale = locale;
|
|
57
|
+
this.intl = createIntl({ defaultLocale: this.defaultLocale, locale });
|
|
58
|
+
this.emitLocaleChange(locale);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
emitLocaleChange(locale: string) {
|
|
62
|
+
for (const callback of this._localeSubscriptions) {
|
|
63
|
+
callback(locale);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Subscribe to changes in the current locale for this manager.
|
|
69
|
+
*/
|
|
70
|
+
onLocaleChange = (callback: (locale: string) => void) => {
|
|
71
|
+
this._localeSubscriptions.add(callback);
|
|
72
|
+
return () => this._localeSubscriptions.delete(callback);
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Format the given message in the current locale with the provided values.
|
|
77
|
+
* The returned values is _always_ an Array of parts, even if the message is
|
|
78
|
+
* a simple string value.
|
|
79
|
+
*
|
|
80
|
+
* This function is the basis of how messages are normally formatted, and can
|
|
81
|
+
* be used anywhere. However, it is not reactive and only functions on the
|
|
82
|
+
* data that is currently loaded and known. For a reactive function that
|
|
83
|
+
* automatically updates when the locale changes or when new data is loaded,
|
|
84
|
+
* use `format`, which will wrap the formatting in a React component that
|
|
85
|
+
* subscribes to the current locale and state of loaded messages.
|
|
86
|
+
*/
|
|
87
|
+
formatToParts<T extends TypedIntlMessageGetter<object | undefined>>(
|
|
88
|
+
message: T,
|
|
89
|
+
): Array<string | any>;
|
|
90
|
+
formatToParts<T extends TypedIntlMessageGetter<object | undefined>>(
|
|
91
|
+
message: T,
|
|
92
|
+
values: Omit<FormatValuesFor<T>, DefaultValues>,
|
|
93
|
+
): Array<string | any>;
|
|
94
|
+
formatToParts<T extends TypedIntlMessageGetter<object | undefined>>(
|
|
95
|
+
message: T,
|
|
96
|
+
values?: Omit<FormatValuesFor<T>, DefaultValues>,
|
|
97
|
+
) {
|
|
98
|
+
if (typeof message === 'string') return [message];
|
|
99
|
+
const resolvedMessage = typeof message === 'function' ? message(this.currentLocale) : message;
|
|
100
|
+
if (typeof resolvedMessage === 'string') return [resolvedMessage];
|
|
101
|
+
|
|
102
|
+
const resolvedValues =
|
|
103
|
+
values != null
|
|
104
|
+
? { ...this.defaultRichTextElements, ...values }
|
|
105
|
+
: this.defaultRichTextElements;
|
|
106
|
+
const parts = resolvedMessage.formatToParts(
|
|
107
|
+
this.intl.formatters as Formatters,
|
|
108
|
+
this.intl.formats as Formats,
|
|
109
|
+
resolvedValues,
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
const result = [];
|
|
113
|
+
let inLiteral = false;
|
|
114
|
+
for (const part of parts) {
|
|
115
|
+
// This condition merges consecutive literal elements (static strings)
|
|
116
|
+
// into single parts to reduce the number of nodes in the result. This
|
|
117
|
+
// condition will never be true on the first loop, ensuring that the
|
|
118
|
+
// array must at least have one entry before it attempts to concatenate
|
|
119
|
+
// onto that value again.
|
|
120
|
+
if (inLiteral && (inLiteral = part.type === FormatPartType.literal)) {
|
|
121
|
+
result[result.length - 1] += part.value;
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
inLiteral = part.type === FormatPartType.literal;
|
|
126
|
+
result.push(part.value);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return result;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Format the given message with the provided values, removing any styling
|
|
134
|
+
* and non-textual content from the message, returning a plain string.
|
|
135
|
+
*/
|
|
136
|
+
formatToPlainString<T extends TypedIntlMessageGetter<object | undefined>>(message: T): string;
|
|
137
|
+
formatToPlainString<T extends TypedIntlMessageGetter<object | undefined>>(
|
|
138
|
+
message: T,
|
|
139
|
+
values: Omit<FormatValuesFor<T>, DefaultValues>,
|
|
140
|
+
): string;
|
|
141
|
+
formatToPlainString<T extends TypedIntlMessageGetter<object | undefined>>(
|
|
142
|
+
message: T,
|
|
143
|
+
values?: Omit<FormatValuesFor<T>, DefaultValues>,
|
|
144
|
+
) {
|
|
145
|
+
if (typeof message === 'string') return message;
|
|
146
|
+
const resolvedMessage = message(this.currentLocale);
|
|
147
|
+
if (typeof resolvedMessage === 'string') return resolvedMessage;
|
|
148
|
+
|
|
149
|
+
// No need to pass in `defaultRichTextElements`, since the stylistic tags
|
|
150
|
+
// will be removed from the string anyway.
|
|
151
|
+
return resolvedMessage.formatToPlainString(
|
|
152
|
+
this.intl.formatters as Formatters,
|
|
153
|
+
this.intl.formats as Formats,
|
|
154
|
+
values,
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Create a new MessageLoader, which handles lazily loading messages for
|
|
161
|
+
* different locales and sanity checks as needed to provide accessors for each
|
|
162
|
+
* message defined in `messageKeys`.
|
|
163
|
+
*/
|
|
164
|
+
export function createLoader(messageKeys: string[], localeImportMap: LocaleImportMap) {
|
|
165
|
+
return new MessageLoader(messageKeys, localeImportMap);
|
|
166
|
+
}
|