@discord/intl 0.1.3-rc.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/format.d.ts +30 -0
- package/dist/format.js +214 -0
- package/dist/formatters/ast.d.ts +67 -0
- package/dist/formatters/ast.js +148 -0
- package/dist/formatters/index.d.ts +4 -0
- package/dist/formatters/index.js +14 -0
- package/dist/formatters/markdown.d.ts +13 -0
- package/dist/formatters/markdown.js +80 -0
- package/dist/formatters/react.d.ts +28 -0
- package/dist/formatters/react.js +111 -0
- package/dist/formatters/string.d.ts +21 -0
- package/dist/formatters/string.js +90 -0
- package/dist/index.d.ts +6 -6
- package/dist/index.js +16 -3
- package/dist/intl-manager.d.ts +25 -56
- package/dist/intl-manager.js +58 -79
- package/dist/keyless-json.d.ts +8 -1
- package/dist/message-loader.d.ts +2 -3
- package/dist/message.d.ts +6 -27
- package/dist/message.js +29 -78
- package/dist/types.d.ts +41 -45
- package/package.json +1 -2
- package/src/format.ts +224 -0
- package/src/formatters/ast.ts +155 -0
- package/src/formatters/index.ts +9 -0
- package/src/formatters/markdown.ts +54 -0
- package/src/formatters/react.ts +100 -0
- package/src/formatters/string.ts +62 -0
- package/src/index.ts +6 -6
- package/src/intl-manager.ts +70 -148
- package/src/keyless-json.ts +7 -0
- package/src/message-loader.ts +2 -4
- package/src/message.ts +16 -75
- package/src/types.d.ts +41 -45
- package/dist/react.d.ts +0 -38
- package/dist/react.js +0 -80
- package/src/react.ts +0 -80
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AnyIntlMessage,
|
|
3
|
+
FormatterImplementation,
|
|
4
|
+
FunctionTypes,
|
|
5
|
+
type RichTextFormattingMap,
|
|
6
|
+
RichTextTagNames,
|
|
7
|
+
} from '../types';
|
|
8
|
+
import { IntlManager } from '../intl-manager';
|
|
9
|
+
import { FormatBuilder } from '../format';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Types for formatting functions when calling `formatToParts`, ensuring the
|
|
13
|
+
* functions yield value AST nodes.
|
|
14
|
+
*/
|
|
15
|
+
export type AstFunctionTypes = FunctionTypes<RichTextNode>;
|
|
16
|
+
|
|
17
|
+
// This structure aims to match `simple-markdown`'s AST, but that type is very
|
|
18
|
+
// loosely defined as just "type" and "arbitrary map of content". This is more
|
|
19
|
+
// explicit and type-safe.
|
|
20
|
+
export enum RichTextNodeType {
|
|
21
|
+
Text = 'text',
|
|
22
|
+
Strong = 'strong',
|
|
23
|
+
Emphasis = 'emphasis',
|
|
24
|
+
Code = 'code',
|
|
25
|
+
Link = 'link',
|
|
26
|
+
Paragraph = 'paragraph',
|
|
27
|
+
/**
|
|
28
|
+
* A special extension for when the value of a placeholder is an object.
|
|
29
|
+
* Rather than casting to a string here, the object is preserved as-is for a
|
|
30
|
+
* future formatter to render as appropriate (i.e., using an object as a
|
|
31
|
+
* value when formatting to an AST should keep that object structure so that
|
|
32
|
+
* the final AST is accurate for what the user expects, but when rendering to
|
|
33
|
+
* a plain string, the final formatter can just cast that object to a string
|
|
34
|
+
* using its `toString` method or some other mechanism).
|
|
35
|
+
*/
|
|
36
|
+
Object = 'object',
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const RICH_TEXT_TAG_TYPES: Record<RichTextTagNames, RichTextNodeType> = {
|
|
40
|
+
$_: RichTextNodeType.Text,
|
|
41
|
+
$b: RichTextNodeType.Strong,
|
|
42
|
+
$i: RichTextNodeType.Emphasis,
|
|
43
|
+
$code: RichTextNodeType.Code,
|
|
44
|
+
$link: RichTextNodeType.Link,
|
|
45
|
+
$p: RichTextNodeType.Paragraph,
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
interface RichTextNodeBase<ContentType> {
|
|
49
|
+
type: RichTextNodeType;
|
|
50
|
+
content: ContentType;
|
|
51
|
+
}
|
|
52
|
+
interface RichTextTextNode extends RichTextNodeBase<string> {
|
|
53
|
+
type: RichTextNodeType.Text;
|
|
54
|
+
}
|
|
55
|
+
interface RichTextStrongNode extends RichTextNodeBase<RichTextNode[]> {
|
|
56
|
+
type: RichTextNodeType.Strong;
|
|
57
|
+
}
|
|
58
|
+
interface RichTextEmphasisNode extends RichTextNodeBase<RichTextNode[]> {
|
|
59
|
+
type: RichTextNodeType.Emphasis;
|
|
60
|
+
}
|
|
61
|
+
interface RichTextCodeNode extends RichTextNodeBase<RichTextNode[]> {
|
|
62
|
+
type: RichTextNodeType.Code;
|
|
63
|
+
}
|
|
64
|
+
interface RichTextParagraphNode extends RichTextNodeBase<RichTextNode[]> {
|
|
65
|
+
type: RichTextNodeType.Paragraph;
|
|
66
|
+
}
|
|
67
|
+
interface RichTextLinkNode extends RichTextNodeBase<RichTextNode[]> {
|
|
68
|
+
type: RichTextNodeType.Link;
|
|
69
|
+
target: string;
|
|
70
|
+
}
|
|
71
|
+
interface RichTextObjectNode extends RichTextNodeBase<object> {
|
|
72
|
+
type: RichTextNodeType.Object;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export type RichTextNode =
|
|
76
|
+
| RichTextTextNode
|
|
77
|
+
| RichTextStrongNode
|
|
78
|
+
| RichTextEmphasisNode
|
|
79
|
+
| RichTextCodeNode
|
|
80
|
+
| RichTextParagraphNode
|
|
81
|
+
| RichTextLinkNode
|
|
82
|
+
| RichTextObjectNode;
|
|
83
|
+
|
|
84
|
+
const AST_RICH_TEXT_ELEMENTS: RichTextFormattingMap<AstFunctionTypes['hook']> = {
|
|
85
|
+
$_: () => ({ type: RichTextNodeType.Text, content: '' }),
|
|
86
|
+
$b: (content) => ({ type: RichTextNodeType.Strong, content }),
|
|
87
|
+
$code: (content) => ({ type: RichTextNodeType.Code, content }),
|
|
88
|
+
$i: (content) => ({ type: RichTextNodeType.Emphasis, content }),
|
|
89
|
+
$link: ([target, ...content]) => ({
|
|
90
|
+
type: RichTextNodeType.Link,
|
|
91
|
+
target: target.content as string,
|
|
92
|
+
content,
|
|
93
|
+
}),
|
|
94
|
+
$p: (content) => ({ type: RichTextNodeType.Paragraph, content }),
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
class AstBuilder extends FormatBuilder<RichTextNode> {
|
|
98
|
+
result: RichTextNode[] = [];
|
|
99
|
+
|
|
100
|
+
pushRichTextTag(tag: RichTextTagNames, children: RichTextNode[]) {
|
|
101
|
+
if (!(tag in AST_RICH_TEXT_ELEMENTS)) {
|
|
102
|
+
throw `${tag} is not a known rich text formatting tag`;
|
|
103
|
+
}
|
|
104
|
+
const result = AST_RICH_TEXT_ELEMENTS[tag](children);
|
|
105
|
+
if (Array.isArray(result)) {
|
|
106
|
+
this.result.push(...result);
|
|
107
|
+
} else {
|
|
108
|
+
this.result.push(result);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
pushLiteralText(text: string) {
|
|
113
|
+
const last = this.result[this.result.length - 1];
|
|
114
|
+
if (last.type === RichTextNodeType.Text) {
|
|
115
|
+
last.content += text;
|
|
116
|
+
} else {
|
|
117
|
+
this.result.push({ type: RichTextNodeType.Text, content: text });
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
pushObject(value: object) {
|
|
122
|
+
this.result.push({ type: RichTextNodeType.Object, content: value });
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
finish(): RichTextNode[] {
|
|
126
|
+
return this.result;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Format the given message in the current locale with the provided values.
|
|
132
|
+
* The returned values is _always_ an Array of parts, even if the message is
|
|
133
|
+
* a simple string value.
|
|
134
|
+
*
|
|
135
|
+
* This function is the basis of how messages are normally formatted, and can
|
|
136
|
+
* be used anywhere. However, it is not reactive and only functions on the
|
|
137
|
+
* data that is currently loaded and known. For a reactive function that
|
|
138
|
+
* automatically updates when the locale changes or when new data is loaded,
|
|
139
|
+
* use `format`, which will wrap the formatting in a React component that
|
|
140
|
+
* subscribes to the current locale and state of loaded messages.
|
|
141
|
+
*/
|
|
142
|
+
export function formatToAst(
|
|
143
|
+
this: IntlManager,
|
|
144
|
+
message: AnyIntlMessage,
|
|
145
|
+
values: object,
|
|
146
|
+
): RichTextNode[] {
|
|
147
|
+
if (typeof message === 'string') return [{ type: RichTextNodeType.Text, content: message }];
|
|
148
|
+
|
|
149
|
+
return this.bindFormatValues(AstBuilder, message, values);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export const astFormatter: FormatterImplementation<AstFunctionTypes, RichTextNode[]> = {
|
|
153
|
+
format: formatToAst,
|
|
154
|
+
builder: AstBuilder,
|
|
155
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export { astFormatter, type AstFunctionTypes, type RichTextNode, RichTextNodeType } from './ast';
|
|
2
|
+
export { markdownFormatter, type MarkdownFunctionTypes } from './markdown';
|
|
3
|
+
export {
|
|
4
|
+
reactFormatter,
|
|
5
|
+
makeReactFormatter,
|
|
6
|
+
DEFAULT_REACT_RICH_TEXT_ELEMENTS,
|
|
7
|
+
type ReactFunctionTypes,
|
|
8
|
+
} from './react';
|
|
9
|
+
export { stringFormatter, type StringFunctionTypes } from './string';
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Similar to `formatToPlainString`, format the given message with the provided
|
|
3
|
+
* values, but convert all rich text formatting back to Markdown syntax rather
|
|
4
|
+
* than rendering the actual rich content. The result is a plain string that
|
|
5
|
+
* can be sent through a separate Markdown renderer to get an equivalent
|
|
6
|
+
* result to formatting this message directly.
|
|
7
|
+
*/
|
|
8
|
+
import {
|
|
9
|
+
AnyIntlMessage,
|
|
10
|
+
FormatterImplementation,
|
|
11
|
+
FunctionTypes,
|
|
12
|
+
RichTextFormattingMap,
|
|
13
|
+
RichTextTagNames,
|
|
14
|
+
} from '../types';
|
|
15
|
+
import { IntlManager } from '../intl-manager';
|
|
16
|
+
import { FormatBuilderConstructor } from '../format';
|
|
17
|
+
import { StringBuilder } from './string';
|
|
18
|
+
|
|
19
|
+
export type MarkdownFunctionTypes = FunctionTypes<string>;
|
|
20
|
+
|
|
21
|
+
const MARKDOWN_RICH_TEXT_ELEMENTS: RichTextFormattingMap<MarkdownFunctionTypes['hook']> = {
|
|
22
|
+
$_: () => '',
|
|
23
|
+
$b: (content) => '**' + content.join('') + '**',
|
|
24
|
+
$code: (content) => '`' + content.join('') + '`',
|
|
25
|
+
$i: (content) => '*' + content.join('') + '*',
|
|
26
|
+
$link: ([target, ...content]) => '[' + content.join('') + '](' + target + ')',
|
|
27
|
+
$p: (content) => content.join('') + '\n\n',
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
class MarkdownBuilder extends StringBuilder {
|
|
31
|
+
result: string = '';
|
|
32
|
+
|
|
33
|
+
pushRichTextTag(tag: RichTextTagNames, children: string[]) {
|
|
34
|
+
this.result += MARKDOWN_RICH_TEXT_ELEMENTS[tag](children);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function formatToMarkdownString(
|
|
39
|
+
this: IntlManager,
|
|
40
|
+
message: AnyIntlMessage,
|
|
41
|
+
values: object,
|
|
42
|
+
Builder: FormatBuilderConstructor<string> = MarkdownBuilder,
|
|
43
|
+
): string {
|
|
44
|
+
if (typeof message === 'string') return message;
|
|
45
|
+
|
|
46
|
+
const result = this.bindFormatValues(Builder, message, values);
|
|
47
|
+
// MarkdownBuilder always creates a single-element array with the string value.
|
|
48
|
+
return result[0];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export const markdownFormatter: FormatterImplementation<MarkdownFunctionTypes, string> = {
|
|
52
|
+
format: formatToMarkdownString,
|
|
53
|
+
builder: MarkdownBuilder,
|
|
54
|
+
};
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Format the given message as a React component, allowing it to listen for
|
|
3
|
+
* and respond to updates about the current locale and other relevant
|
|
4
|
+
* information.
|
|
5
|
+
*/
|
|
6
|
+
import type {
|
|
7
|
+
AnyIntlMessage,
|
|
8
|
+
FormatterImplementation,
|
|
9
|
+
FunctionTypes,
|
|
10
|
+
RichTextFormattingMap,
|
|
11
|
+
RichTextTagNames,
|
|
12
|
+
} from '../types';
|
|
13
|
+
import * as React from 'react';
|
|
14
|
+
import { IntlManager } from '../intl-manager';
|
|
15
|
+
import { FormatBuilder, FormatBuilderConstructor } from '../format';
|
|
16
|
+
|
|
17
|
+
export type ReactHandlerEvent = React.MouseEvent | React.KeyboardEvent;
|
|
18
|
+
export type ReactClickHandler = (e: ReactHandlerEvent) => void;
|
|
19
|
+
|
|
20
|
+
export type ReactFunctionTypes = FunctionTypes<
|
|
21
|
+
React.ReactNode,
|
|
22
|
+
| ReactClickHandler
|
|
23
|
+
| { onClick: ReactClickHandler }
|
|
24
|
+
| { onContextMenu: ReactClickHandler }
|
|
25
|
+
| { onClick: ReactClickHandler; onContextMenu: ReactClickHandler }
|
|
26
|
+
>;
|
|
27
|
+
|
|
28
|
+
const h = React.createElement;
|
|
29
|
+
export const DEFAULT_REACT_RICH_TEXT_ELEMENTS: RichTextFormattingMap<ReactFunctionTypes['hook']> = {
|
|
30
|
+
$_: () => '',
|
|
31
|
+
$b: (content) => h('strong', undefined, content),
|
|
32
|
+
$code: (content) => h('code', undefined, content),
|
|
33
|
+
$i: (content) => h('em', undefined, content),
|
|
34
|
+
// $link will always be [href, <empty>, ...content]
|
|
35
|
+
$link: ([href, ...content]) => h('a', { href }, content),
|
|
36
|
+
$p: (content) => h('p', undefined, content),
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Creates a new `FormatBuilder` class that constructs a React element tree
|
|
41
|
+
* from the message using the given `richTextElements` to apply formatting.
|
|
42
|
+
* This allows consumers to inject their own design system components and
|
|
43
|
+
* overrides for rendering elements like links, paragraphs, and code blocks.
|
|
44
|
+
* @param richTextElements
|
|
45
|
+
*/
|
|
46
|
+
function createReactBuilder(richTextElements: RichTextFormattingMap<ReactFunctionTypes['hook']>): {
|
|
47
|
+
new (): FormatBuilder<React.ReactNode>;
|
|
48
|
+
} {
|
|
49
|
+
return class extends FormatBuilder<React.ReactNode> {
|
|
50
|
+
result: React.ReactNode[] = [];
|
|
51
|
+
|
|
52
|
+
pushRichTextTag(tag: RichTextTagNames, children: React.ReactNode[]) {
|
|
53
|
+
this.result.push(richTextElements[tag](children));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
pushLiteralText(text: string) {
|
|
57
|
+
this.result.push(text);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
pushObject(value: object) {
|
|
61
|
+
// @ts-expect-error this is technically invalid, but we'll just assume that if a format returns
|
|
62
|
+
// an object, it'll be acting as some form of ReactNode.
|
|
63
|
+
this.result.push(value);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
finish(): React.ReactNode[] {
|
|
67
|
+
return this.result;
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function formatReact(
|
|
73
|
+
this: IntlManager,
|
|
74
|
+
message: AnyIntlMessage,
|
|
75
|
+
values: object,
|
|
76
|
+
Builder: FormatBuilderConstructor<React.ReactElement>,
|
|
77
|
+
): React.ReactElement {
|
|
78
|
+
if (typeof message === 'string') {
|
|
79
|
+
return React.createElement(React.Fragment, undefined, message);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const parts = this.bindFormatValues(Builder, message, values);
|
|
83
|
+
return React.createElement(React.Fragment, undefined, parts);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Create a new React formatter with the given rich text elements replacing the defaults. Use this
|
|
88
|
+
* function to inject custom components for things like links and paragraphs, which may be best
|
|
89
|
+
* suited to use components from a Design System or other library rather than native DOM elements.
|
|
90
|
+
*/
|
|
91
|
+
export function makeReactFormatter(
|
|
92
|
+
richTextElements: RichTextFormattingMap<ReactFunctionTypes['hook']>,
|
|
93
|
+
): FormatterImplementation<ReactFunctionTypes, React.ReactElement, React.ReactNode> {
|
|
94
|
+
return {
|
|
95
|
+
format: formatReact,
|
|
96
|
+
builder: createReactBuilder(richTextElements),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export const reactFormatter = makeReactFormatter(DEFAULT_REACT_RICH_TEXT_ELEMENTS);
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Format the given message with the provided values, removing any styling
|
|
3
|
+
* and non-textual content from the message, returning a plain string.
|
|
4
|
+
*/
|
|
5
|
+
import { AnyIntlMessage, FormatterImplementation, FunctionTypes, RichTextTagNames } from '../types';
|
|
6
|
+
import { FormatBuilder } from '../format';
|
|
7
|
+
import { IntlManager } from '../intl-manager';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Types for formatting functions when calling `bindFormatValues`, ensuring the
|
|
11
|
+
* functions always yield plain strings.
|
|
12
|
+
*/
|
|
13
|
+
export type StringFunctionTypes = FunctionTypes<string>;
|
|
14
|
+
|
|
15
|
+
export class StringBuilder extends FormatBuilder<string> {
|
|
16
|
+
result: string = '';
|
|
17
|
+
|
|
18
|
+
pushRichTextTag(tag: RichTextTagNames, children: string[]) {
|
|
19
|
+
// Plain string formatting ignores rich text tags and just takes the
|
|
20
|
+
// visible content from the children. This means the `target` element
|
|
21
|
+
// of a link is ignored, otherwise all the children are just joined
|
|
22
|
+
// together directly.
|
|
23
|
+
if (tag === '$link') {
|
|
24
|
+
this.result += children.slice(1).join('');
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
this.result += children.join('');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
pushLiteralText(text: string) {
|
|
31
|
+
this.result += text;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
pushObject(value: object) {
|
|
35
|
+
// Objects are only included in the result if they specify a toString value directly.
|
|
36
|
+
// Otherwise, they would be rendered as `[object Object]`, which is never helpful.
|
|
37
|
+
if ('toString' in value) {
|
|
38
|
+
this.result += value.toString();
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
finish(): string[] {
|
|
43
|
+
return [this.result];
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function formatToPlainString(
|
|
48
|
+
this: IntlManager,
|
|
49
|
+
message: AnyIntlMessage,
|
|
50
|
+
values: object,
|
|
51
|
+
): string {
|
|
52
|
+
if (typeof message === 'string') return message;
|
|
53
|
+
|
|
54
|
+
const result = this.bindFormatValues(StringBuilder, message, values);
|
|
55
|
+
// StringBuilder always creates a single element array with the string value.
|
|
56
|
+
return result[0];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export const stringFormatter: FormatterImplementation<StringFunctionTypes, string> = {
|
|
60
|
+
format: formatToPlainString,
|
|
61
|
+
builder: StringBuilder,
|
|
62
|
+
};
|
package/src/index.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { RichTextNode } from './formatters';
|
|
3
3
|
|
|
4
|
-
export { IntlManager, createLoader, DEFAULT_LOCALE } from './intl-manager';
|
|
5
|
-
export
|
|
4
|
+
export { IntlManager, createLoader, DEFAULT_LOCALE, type FormatFunction } from './intl-manager';
|
|
5
|
+
export * from './formatters';
|
|
6
6
|
|
|
7
7
|
export type * from './types.d.ts';
|
|
8
8
|
|
|
@@ -21,7 +21,7 @@ export type * from './types.d.ts';
|
|
|
21
21
|
* intl message, even if the actual value comes from elsewhere (like a
|
|
22
22
|
* user-generated string).
|
|
23
23
|
*/
|
|
24
|
-
export type ReactIntlMessage = string |
|
|
24
|
+
export type ReactIntlMessage = string | React.ReactElement;
|
|
25
25
|
|
|
26
26
|
/**
|
|
27
27
|
* The return value of `formatToParts` from `@discord/intl`, this type
|
|
@@ -33,4 +33,4 @@ export type ReactIntlMessage = string | ReturnType<IntlManagerReact<any>['format
|
|
|
33
33
|
* from a call to `formatToParts`, meaning they are intended to be fully static
|
|
34
34
|
* structures passed around for custom rendering functions to use.
|
|
35
35
|
*/
|
|
36
|
-
export type IntlMessageAst =
|
|
36
|
+
export type IntlMessageAst = RichTextNode;
|
package/src/intl-manager.ts
CHANGED
|
@@ -1,30 +1,16 @@
|
|
|
1
1
|
import { createIntl, IntlShape } from '@formatjs/intl';
|
|
2
|
-
import {
|
|
3
|
-
Formats,
|
|
4
|
-
Formatters,
|
|
5
|
-
IntlMessageFormat,
|
|
6
|
-
PART_TYPE as FormatPartType,
|
|
7
|
-
} from 'intl-messageformat';
|
|
2
|
+
import { Formats, Formatters, IntlMessageFormat } from 'intl-messageformat';
|
|
8
3
|
|
|
9
4
|
import { LocaleId, LocaleImportMap, MessageLoader } from './message-loader';
|
|
10
5
|
|
|
11
6
|
import type {
|
|
12
|
-
|
|
7
|
+
FormatterImplementation,
|
|
13
8
|
IntlMessageGetter,
|
|
14
9
|
RequiredFormatValues,
|
|
15
|
-
RichTextElementMap,
|
|
16
10
|
TypedIntlMessageGetter,
|
|
17
11
|
} from './types';
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
* Types for formatting functions when calling `formatToParts`, ensuring the
|
|
21
|
-
* functions yield value AST nodes.
|
|
22
|
-
*/
|
|
23
|
-
type AstFunctionTypes = {
|
|
24
|
-
link: undefined | object | ((children: object) => Array<string | any>);
|
|
25
|
-
hook: undefined | object | ((children: object) => Array<string | any>);
|
|
26
|
-
handler: undefined | object | ((children: object) => Array<string | any>);
|
|
27
|
-
};
|
|
12
|
+
import { InternalIntlMessage } from './message';
|
|
13
|
+
import { bindFormatValues, FormatBuilderConstructor } from './format';
|
|
28
14
|
|
|
29
15
|
/**
|
|
30
16
|
* Fallback locale used for all internationalization when an operation in the
|
|
@@ -32,26 +18,35 @@ type AstFunctionTypes = {
|
|
|
32
18
|
*/
|
|
33
19
|
export const DEFAULT_LOCALE: string = 'en-US';
|
|
34
20
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
21
|
+
type FormatterReturnType<F extends FormatterImplementation<any, any>> =
|
|
22
|
+
F extends FormatterImplementation<any, infer Return> ? Return : never;
|
|
23
|
+
|
|
24
|
+
type FormatterFunctionTypes<F extends FormatterImplementation<any, any>> =
|
|
25
|
+
F extends FormatterImplementation<infer Functions, any> ? Functions : never;
|
|
26
|
+
|
|
27
|
+
export type FormatFunction<F extends FormatterImplementation<any, any>> = <
|
|
28
|
+
T extends IntlMessageGetter,
|
|
29
|
+
>(
|
|
30
|
+
this: IntlManager,
|
|
31
|
+
message: T,
|
|
32
|
+
values: RequiredFormatValues<T, FormatterFunctionTypes<F>>,
|
|
33
|
+
) => FormatterReturnType<F>;
|
|
34
|
+
|
|
35
|
+
type ThisWithFormatters<
|
|
36
|
+
This,
|
|
37
|
+
T extends Record<string, FormatterImplementation<any, any>>,
|
|
38
|
+
> = This & {
|
|
39
|
+
[K in keyof T]: FormatFunction<T[K]>;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export class IntlManager {
|
|
39
43
|
defaultLocale: string;
|
|
40
44
|
currentLocale: string;
|
|
41
45
|
intl: IntlShape;
|
|
42
46
|
|
|
43
|
-
/**
|
|
44
|
-
* When formatting tag elements like `<b>value</b>`, these functions will
|
|
45
|
-
* automatically be used by all messages, unless they are explicitly removed
|
|
46
|
-
* for a specific call, either by overriding the value or by calling a
|
|
47
|
-
* dedicated method like `formatToPlainString` which will remove all
|
|
48
|
-
* rich elements from the string and return only the plain text result.
|
|
49
|
-
*/
|
|
50
|
-
defaultRichTextElements: DefaultElements;
|
|
51
|
-
|
|
52
47
|
_localeSubscriptions: Set<(locale: string) => void>;
|
|
53
48
|
|
|
54
|
-
constructor(defaultLocale: string = DEFAULT_LOCALE
|
|
49
|
+
constructor(defaultLocale: string = DEFAULT_LOCALE) {
|
|
55
50
|
this.defaultLocale = defaultLocale;
|
|
56
51
|
this.currentLocale = defaultLocale;
|
|
57
52
|
this.intl = createIntl({
|
|
@@ -60,10 +55,38 @@ export class IntlManager<
|
|
|
60
55
|
locale: defaultLocale,
|
|
61
56
|
});
|
|
62
57
|
|
|
63
|
-
this.defaultRichTextElements = defaultRichTextElements;
|
|
64
58
|
this._localeSubscriptions = new Set();
|
|
65
59
|
}
|
|
66
60
|
|
|
61
|
+
/**
|
|
62
|
+
* Add a set of formatter implementations to this manager, making each available as a direct
|
|
63
|
+
* property
|
|
64
|
+
*/
|
|
65
|
+
withFormatters<const T extends Record<string, FormatterImplementation<any, any>>>(
|
|
66
|
+
formatters: T,
|
|
67
|
+
): ThisWithFormatters<this, T> {
|
|
68
|
+
for (const [name, formatter] of Object.entries(formatters)) {
|
|
69
|
+
this[name] = this.makeFormatFunction(formatter);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return this as ThisWithFormatters<this, T>;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Return a new function bound to this manager that uses the given `FormatterImplementation` to
|
|
77
|
+
* format a message after it has been resolved for the current locale and potentially processed in
|
|
78
|
+
* other ways by the manager.
|
|
79
|
+
*/
|
|
80
|
+
makeFormatFunction<F extends FormatterImplementation<any, any>>({
|
|
81
|
+
format,
|
|
82
|
+
builder,
|
|
83
|
+
}: F): FormatFunction<FormatterReturnType<F>> {
|
|
84
|
+
const formatter = format.bind(this);
|
|
85
|
+
return (message, values) => {
|
|
86
|
+
return formatter(message(this.currentLocale), values, builder);
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
67
90
|
/**
|
|
68
91
|
* Set the locale for this package to use. This is a global change, and all
|
|
69
92
|
* methods on this object will use this locale for formatting.
|
|
@@ -89,124 +112,23 @@ export class IntlManager<
|
|
|
89
112
|
};
|
|
90
113
|
|
|
91
114
|
/**
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
* a simple string value.
|
|
95
|
-
*
|
|
96
|
-
* This function is the basis of how messages are normally formatted, and can
|
|
97
|
-
* be used anywhere. However, it is not reactive and only functions on the
|
|
98
|
-
* data that is currently loaded and known. For a reactive function that
|
|
99
|
-
* automatically updates when the locale changes or when new data is loaded,
|
|
100
|
-
* use `format`, which will wrap the formatting in a React component that
|
|
101
|
-
* subscribes to the current locale and state of loaded messages.
|
|
115
|
+
* For static messages with no rich text and no dynamic placeholders, use this method to
|
|
116
|
+
* immediately return the plain string value of the message in the current locale.
|
|
102
117
|
*/
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
message
|
|
106
|
-
values: RequiredFormatValues<T, DefaultElements, AstFunctionTypes>,
|
|
107
|
-
): Array<string | any>;
|
|
108
|
-
formatToParts<T extends IntlMessageGetter>(
|
|
109
|
-
message: T,
|
|
110
|
-
values?: RequiredFormatValues<T, DefaultElements, AstFunctionTypes>,
|
|
111
|
-
): Array<string | any> {
|
|
112
|
-
if (typeof message === 'string') return [message];
|
|
113
|
-
const resolvedMessage = typeof message === 'function' ? message(this.currentLocale) : message;
|
|
114
|
-
if (typeof resolvedMessage === 'string') return [resolvedMessage];
|
|
115
|
-
|
|
116
|
-
const resolvedValues =
|
|
117
|
-
values != null
|
|
118
|
-
? { ...this.defaultRichTextElements, ...values }
|
|
119
|
-
: this.defaultRichTextElements;
|
|
120
|
-
const parts = resolvedMessage.formatToParts(
|
|
121
|
-
this.intl.formatters as Formatters,
|
|
122
|
-
this.intl.formats as Formats,
|
|
123
|
-
resolvedValues,
|
|
124
|
-
);
|
|
125
|
-
|
|
126
|
-
const result = [];
|
|
127
|
-
let inLiteral = false;
|
|
128
|
-
for (const part of parts) {
|
|
129
|
-
// This condition merges consecutive literal elements (static strings)
|
|
130
|
-
// into single parts to reduce the number of nodes in the result. This
|
|
131
|
-
// condition will never be true on the first loop, ensuring that the
|
|
132
|
-
// array must at least have one entry before it attempts to concatenate
|
|
133
|
-
// onto that value again.
|
|
134
|
-
if (inLiteral && (inLiteral = part.type === FormatPartType.literal)) {
|
|
135
|
-
result[result.length - 1] += part.value;
|
|
136
|
-
continue;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
inLiteral = part.type === FormatPartType.literal;
|
|
140
|
-
result.push(part.value);
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
return result;
|
|
118
|
+
string<T extends TypedIntlMessageGetter<undefined>>(message: T): string {
|
|
119
|
+
// TODO: Figure out how to make this typing exact.
|
|
120
|
+
return message(this.currentLocale).message;
|
|
144
121
|
}
|
|
145
122
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
string(message: TypedIntlMessageGetter<undefined>): string {
|
|
156
|
-
// TODO(faulty): Optimize this so that plain strings are hotpathed even faster.
|
|
157
|
-
return this.formatToPlainString(message);
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
/**
|
|
161
|
-
* Format the given message with the provided values, removing any styling
|
|
162
|
-
* and non-textual content from the message, returning a plain string.
|
|
163
|
-
*/
|
|
164
|
-
formatToPlainString<T extends TypedIntlMessageGetter<undefined>>(message: T): string;
|
|
165
|
-
formatToPlainString<T extends IntlMessageGetter>(
|
|
166
|
-
message: T,
|
|
167
|
-
values: RequiredFormatValues<T, DefaultElements, FunctionTypes>,
|
|
168
|
-
): string;
|
|
169
|
-
formatToPlainString<T extends IntlMessageGetter>(
|
|
170
|
-
message: T,
|
|
171
|
-
values?: RequiredFormatValues<T, DefaultElements, FunctionTypes>,
|
|
172
|
-
): string {
|
|
173
|
-
if (typeof message === 'string') return message;
|
|
174
|
-
const resolvedMessage = message(this.currentLocale);
|
|
175
|
-
if (typeof resolvedMessage === 'string') return resolvedMessage;
|
|
176
|
-
|
|
177
|
-
// No need to pass in `defaultRichTextElements`, since the stylistic tags
|
|
178
|
-
// will be removed from the string anyway.
|
|
179
|
-
return resolvedMessage.formatToPlainString(
|
|
180
|
-
this.intl.formatters as Formatters,
|
|
181
|
-
this.intl.formats as Formats,
|
|
182
|
-
values,
|
|
183
|
-
);
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
/**
|
|
187
|
-
* Similar to `formatToPlainString`, format the given message with the provided
|
|
188
|
-
* values, but convert all rich text formatting back to Markdown syntax rather
|
|
189
|
-
* than rendering the actual rich content. The result is a plain string that
|
|
190
|
-
* can be sent through a separate Markdown renderer to get an equivalent
|
|
191
|
-
* result to formatting this message directly.
|
|
192
|
-
*/
|
|
193
|
-
formatToMarkdownString<T extends TypedIntlMessageGetter<undefined>>(message: T): string;
|
|
194
|
-
formatToMarkdownString<T extends IntlMessageGetter>(
|
|
195
|
-
message: T,
|
|
196
|
-
values: RequiredFormatValues<T, DefaultElements, FunctionTypes>,
|
|
197
|
-
): string;
|
|
198
|
-
formatToMarkdownString<T extends IntlMessageGetter>(
|
|
199
|
-
message: T,
|
|
200
|
-
values?: RequiredFormatValues<T, DefaultElements, FunctionTypes>,
|
|
201
|
-
): string {
|
|
202
|
-
// TODO(faulty): Implement the markdown syntax conversion here.
|
|
203
|
-
if (typeof message === 'string') return message;
|
|
204
|
-
const resolvedMessage = message(this.currentLocale);
|
|
205
|
-
if (typeof resolvedMessage === 'string') return resolvedMessage;
|
|
206
|
-
|
|
207
|
-
// No need to pass in `defaultRichTextElements`, since the stylistic tags
|
|
208
|
-
// will be removed from the string anyway.
|
|
209
|
-
return resolvedMessage.formatToPlainString(
|
|
123
|
+
bindFormatValues<T>(
|
|
124
|
+
Builder: FormatBuilderConstructor<T>,
|
|
125
|
+
message: InternalIntlMessage,
|
|
126
|
+
values: Record<string, any>,
|
|
127
|
+
): T[] {
|
|
128
|
+
return bindFormatValues(
|
|
129
|
+
Builder,
|
|
130
|
+
message.ast,
|
|
131
|
+
[this.currentLocale, this.defaultLocale],
|
|
210
132
|
this.intl.formatters as Formatters,
|
|
211
133
|
this.intl.formats as Formats,
|
|
212
134
|
values,
|