@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.
@@ -38,6 +38,12 @@ export interface FormatJsTime {
38
38
  style?: string;
39
39
  }
40
40
 
41
+ export interface FormatJsSelect {
42
+ type: FormatJsNodeType.Select;
43
+ value: string;
44
+ options: Record<string, { value: FormatJsNode[] }>;
45
+ }
46
+
41
47
  export type FormatJsPluralType = 'cardinal' | 'ordinal';
42
48
 
43
49
  export interface FormatJsPlural {
@@ -64,6 +70,7 @@ export type FormatJsNode =
64
70
  | FormatJsNumber
65
71
  | FormatJsDate
66
72
  | FormatJsTime
73
+ | FormatJsSelect
67
74
  | FormatJsPlural
68
75
  | FormatJsPound
69
76
  | FormatJsTag;
@@ -1,9 +1,7 @@
1
+ import { FormatJsNode } from './keyless-json';
1
2
  import { InternalIntlMessage } from './message';
2
3
 
3
- import type { MessageFormatElement } from '@formatjs/icu-messageformat-parser';
4
-
5
- type AnyRawMessage = string | MessageFormatElement[];
6
- type MessagesData = Record<string, AnyRawMessage>;
4
+ type MessagesData = Record<string, FormatJsNode[]>;
7
5
  export interface IntlMessageGetterAdditions {
8
6
  onChange(callback: () => void): () => void;
9
7
  }
package/src/message.ts CHANGED
@@ -1,87 +1,28 @@
1
- import {
2
- TYPE as FormatElementType,
3
- parse as parseMessageFormat,
4
- } from '@formatjs/icu-messageformat-parser';
5
- import {
6
- Formats,
7
- Formatters,
8
- MessageFormatPart,
9
- formatToParts as libFormatToParts,
10
- } from 'intl-messageformat';
11
-
12
- import type { MessageFormatElement } from '@formatjs/icu-messageformat-parser';
1
+ import { FormatJsNode, FormatJsNodeType } from './keyless-json';
13
2
 
14
3
  export class InternalIntlMessage {
15
4
  locale: string;
16
5
  message?: string;
17
- ast: MessageFormatElement[];
6
+ ast: FormatJsNode[];
7
+
18
8
  /**
19
9
  * A stripped-down representation of the message with no rich formatting
20
10
  * elements preserved. This will be lazily created the first time it is
21
11
  * requested (e.g., by calling `formatToPlainString`).
22
12
  */
23
- plainAst?: MessageFormatElement[];
13
+ get plainAst(): FormatJsNode[] {
14
+ return (this._plainAst = this._removeRichTags(this.ast));
15
+ }
16
+ _plainAst: FormatJsNode[];
24
17
 
25
- constructor(messageOrAst: string | MessageFormatElement[], locale: string) {
18
+ constructor(messageOrAst: string | FormatJsNode[], locale: string) {
26
19
  this.locale = locale;
27
20
 
28
21
  if (typeof messageOrAst === 'string') {
29
22
  this.message = messageOrAst;
30
- this.ast = parseMessageFormat(messageOrAst);
31
23
  } else {
32
24
  this.ast = messageOrAst;
33
25
  }
34
-
35
- this.plainAst = undefined;
36
- }
37
-
38
- /**
39
- * Format this message with the given locale into an AST of chunks that can
40
- * then be used or rendered into strings, React elements, or anything else as
41
- * needed.
42
- */
43
- formatToParts<T>(
44
- formatters: Formatters,
45
- formats: Formats,
46
- values?: Record<string, any>,
47
- ): Array<MessageFormatPart<T>> {
48
- return libFormatToParts(this.ast, this.locale, formatters, formats, values);
49
- }
50
-
51
- /**
52
- * Formats this message just like `formatToParts`, but strips all stylistic
53
- * elements and tags and returns a plain string with only the literal text of
54
- * the message remaining.
55
- *
56
- * This method works by quickly scanning the string and removing any tag
57
- * elements, formatting, then concatenating the pieces into a single result.
58
- *
59
- * Note that this function does _not_ handle serializing values other than
60
- * plain/primitive values into strings. All values are simply converted using
61
- * the `String()` constructor. So, for example, passing a function as a value
62
- * will directly serialize the function definition into a string, rather than
63
- * calling the function or evaluating the result.
64
- */
65
- formatToPlainString(
66
- formatters: Formatters,
67
- formats: Formats,
68
- values?: Record<string, any>,
69
- ): string {
70
- // Lazily create the reduced AST if it hasn't already been done.
71
- if (this.plainAst == null) {
72
- this.plainAst = [];
73
- for (const part of this.ast) {
74
- this.plainAst.push(...this._removeRichTags(part));
75
- }
76
- }
77
-
78
- const parts = libFormatToParts(this.plainAst, this.locale, formatters, formats, values);
79
- let result = '';
80
- for (const part of parts) {
81
- result += String(part.value);
82
- }
83
-
84
- return result;
85
26
  }
86
27
 
87
28
  /**
@@ -89,15 +30,15 @@ export class InternalIntlMessage {
89
30
  * itself is a tag, the children will be hoisted and returned, otherwise the
90
31
  * element is returned as an array of itself.
91
32
  */
92
- _removeRichTags(element: MessageFormatElement): MessageFormatElement[] {
93
- if (element.type === FormatElementType.tag) {
94
- const childContent = [];
95
- for (const child of element.children) {
96
- childContent.push(...this._removeRichTags(child));
33
+ private _removeRichTags(nodes: FormatJsNode[]): FormatJsNode[] {
34
+ const result = new Array(nodes.length);
35
+ for (const node of nodes) {
36
+ if (node.type === FormatJsNodeType.Tag) {
37
+ result.push(...this._removeRichTags(node.children));
38
+ } else {
39
+ result.push(node);
97
40
  }
98
- return childContent;
99
- } else {
100
- return [element];
101
41
  }
42
+ return result;
102
43
  }
103
44
  }
package/src/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 subtying that TypeScript uses.
89
- (locale: string): InternalIntlMessage | PlainIntlMessage;
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
- ? never
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 IntlOptionalRichTextKeys {
287
- $_: any;
288
- $b: any;
289
- $i: any;
290
- $p: any;
291
- $link: any;
292
- $code: any;
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 IntlFormatFunctionValuesArg<T extends IntlMessageGetter> = RequiredFormatValues<
296
- T,
297
- IntlOptionalRichTextKeys,
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/dist/react.d.ts DELETED
@@ -1,38 +0,0 @@
1
- import * as React from 'react';
2
- import { IntlManager } from './intl-manager';
3
- import type { IntlMessageGetter, RequiredFormatValues, RichTextElementMap, TypedIntlMessageGetter } from './types';
4
- type ReactHandlerEvent = React.MouseEvent | React.KeyboardEvent;
5
- type ReactClickHandler = (e: ReactHandlerEvent) => void;
6
- type ReactFunctionTypes = {
7
- link: undefined | ((content: React.ReactNode | React.ReactNode[], key: string) => React.ReactNode);
8
- hook: undefined | ((content: React.ReactNode | React.ReactNode[], key: string) => React.ReactNode);
9
- handler: undefined | ReactClickHandler | {
10
- onClick: ReactClickHandler;
11
- } | {
12
- onContextMenu: ReactClickHandler;
13
- } | {
14
- onClick: ReactClickHandler;
15
- onContextMenu: ReactClickHandler;
16
- };
17
- };
18
- export declare class IntlManagerReact<DefaultElements extends RichTextElementMap> extends IntlManager<DefaultElements, ReactFunctionTypes> {
19
- /**
20
- * A reactive component form of `intl.format` that automatically updates when
21
- * the application's locale changes and when new data is loaded for the
22
- * subject message.
23
- */
24
- IntlMessage: <T extends IntlMessageGetter>(props: {
25
- message: T;
26
- values?: RequiredFormatValues<T, DefaultElements, ReactFunctionTypes> | never;
27
- }) => string | React.FunctionComponentElement<{
28
- children?: React.ReactNode | undefined;
29
- }>;
30
- /**
31
- * Format the given message as a React component, allowing it to listen for
32
- * and respond to updates about the current locale and other relevant
33
- * information.
34
- */
35
- format<T extends TypedIntlMessageGetter<undefined>>(message: T): React.ReactElement;
36
- format<T extends IntlMessageGetter>(message: T, values: RequiredFormatValues<T, DefaultElements, ReactFunctionTypes>): React.ReactElement;
37
- }
38
- export {};
package/dist/react.js DELETED
@@ -1,80 +0,0 @@
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
- // TODO(faulty): This can and should be replaced with
61
- // `use(messagesLoadedPromise)` once `use` is shipped to stable.
62
- // Source the actual message to render for that locale from its loader.
63
- React.useSyncExternalStore(message.onChange, function () { return message(locale); });
64
- // If there are no object parts in the message, it has no formatting and can just be returned as
65
- // a plain string.
66
- if (typeof message === 'string')
67
- return message;
68
- var parts = _this.formatToParts(message, values);
69
- if (parts.length === 1 && typeof parts[0] === 'string')
70
- return parts[0];
71
- return React.createElement(React.Fragment, undefined, _this.formatToParts(message, values));
72
- };
73
- return _this;
74
- }
75
- IntlManagerReact.prototype.format = function (message, values) {
76
- return React.createElement((this.IntlMessage), { message: message, values: values });
77
- };
78
- return IntlManagerReact;
79
- }(intl_manager_1.IntlManager));
80
- exports.IntlManagerReact = IntlManagerReact;
package/src/react.ts DELETED
@@ -1,80 +0,0 @@
1
- import * as React from 'react';
2
-
3
- import { IntlManager } from './intl-manager';
4
-
5
- import type {
6
- IntlMessageGetter,
7
- RequiredFormatValues,
8
- RichTextElementMap,
9
- TypedIntlMessageGetter,
10
- } from './types';
11
-
12
- type ReactHandlerEvent = React.MouseEvent | React.KeyboardEvent;
13
- type ReactClickHandler = (e: ReactHandlerEvent) => void;
14
-
15
- type ReactFunctionTypes = {
16
- // TODO: Allowing `undefined` on these is a migration artifact that should be
17
- // removed. Functions, like all other values, should be required when
18
- // formatting to avoid unexpected missing behavior.
19
- link:
20
- | undefined
21
- | ((content: React.ReactNode | React.ReactNode[], key: string) => React.ReactNode);
22
-
23
- hook:
24
- | undefined
25
- | ((content: React.ReactNode | React.ReactNode[], key: string) => React.ReactNode);
26
- handler:
27
- | undefined
28
- | ReactClickHandler
29
- | { onClick: ReactClickHandler }
30
- | { onContextMenu: ReactClickHandler }
31
- | { onClick: ReactClickHandler; onContextMenu: ReactClickHandler };
32
- };
33
-
34
- export class IntlManagerReact<DefaultElements extends RichTextElementMap> extends IntlManager<
35
- DefaultElements,
36
- ReactFunctionTypes
37
- > {
38
- /**
39
- * A reactive component form of `intl.format` that automatically updates when
40
- * the application's locale changes and when new data is loaded for the
41
- * subject message.
42
- */
43
- IntlMessage = <T extends IntlMessageGetter>(props: {
44
- message: T;
45
- values?: RequiredFormatValues<T, DefaultElements, ReactFunctionTypes> | never;
46
- }) => {
47
- const { message, values } = props;
48
- // Use the locale from this point in the application, which may be
49
- // different from the global locale.
50
- const locale = React.useSyncExternalStore(this.onLocaleChange, () => this.currentLocale);
51
- // TODO(faulty): This can and should be replaced with
52
- // `use(messagesLoadedPromise)` once `use` is shipped to stable.
53
- // Source the actual message to render for that locale from its loader.
54
- React.useSyncExternalStore(message.onChange, () => message(locale));
55
- // If there are no object parts in the message, it has no formatting and can just be returned as
56
- // a plain string.
57
- if (typeof message === 'string') return message;
58
-
59
- const parts = this.formatToParts(message, values);
60
- if (parts.length === 1 && typeof parts[0] === 'string') return parts[0];
61
- return React.createElement(React.Fragment, undefined, this.formatToParts(message, values));
62
- };
63
-
64
- /**
65
- * Format the given message as a React component, allowing it to listen for
66
- * and respond to updates about the current locale and other relevant
67
- * information.
68
- */
69
- format<T extends TypedIntlMessageGetter<undefined>>(message: T): React.ReactElement;
70
- format<T extends IntlMessageGetter>(
71
- message: T,
72
- values: RequiredFormatValues<T, DefaultElements, ReactFunctionTypes>,
73
- ): React.ReactElement;
74
- format<T extends IntlMessageGetter>(
75
- message: T,
76
- values?: RequiredFormatValues<T, DefaultElements, ReactFunctionTypes>,
77
- ): React.ReactElement {
78
- return React.createElement(this.IntlMessage<T>, { message, values });
79
- }
80
- }