@discord/intl 0.9.1 → 0.10.0-canary.d90454b

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 CHANGED
@@ -21,11 +21,11 @@ import { Formatters } from 'intl-messageformat';
21
21
  import type { FormatConfig } from './format-config';
22
22
  import type { RichTextTagNames } from './types';
23
23
  export declare abstract class FormatBuilder<Result> {
24
- abstract pushRichTextTag(tag: RichTextTagNames, children: Result[]): void;
24
+ abstract pushRichTextTag(tag: RichTextTagNames, children: Result[], control: Result[]): void;
25
25
  abstract pushLiteralText(text: string): void;
26
26
  abstract pushObject(value: object): void;
27
27
  abstract finish(): Result[];
28
28
  }
29
29
  export type FormatBuilderConstructor<Result> = new () => FormatBuilder<Result>;
30
30
  export declare function bindFormatValuesWithBuilder<T, Builder extends FormatBuilder<T>>(builder: Builder, nodes: AstNode[], locales: string | string[], formatters: Formatters, formatConfig: FormatConfig, values?: Record<string, string | object>, currentPluralValue?: number, originalMessage?: string): void;
31
- export declare function bindFormatValues<Result>(Builder: FormatBuilderConstructor<Result>, nodes: AstNode[], locales: string | string[], formatters: Formatters, formatConfig: FormatConfig, values?: Record<string, string | object>, currentPluralValue?: number): Result[];
31
+ export declare function bindFormatValues<Result>(Builder: FormatBuilderConstructor<Result>, nodes: string | AstNode[], locales: string | string[], formatters: Formatters, formatConfig: FormatConfig, values?: Record<string, string | object>, currentPluralValue?: number): Result[];
package/dist/format.js CHANGED
@@ -46,7 +46,7 @@ function bindFormatValuesWithBuilder(builder, nodes, locales, formatters, format
46
46
  builder.pushLiteralText(node);
47
47
  continue;
48
48
  }
49
- const nodeType = node[intl_ast_1.AstNodeIndices.Type];
49
+ const nodeType = node[0 /* AstNodeIndices.Type */];
50
50
  switch (nodeType) {
51
51
  case intl_ast_1.FormatJsNodeType.Pound:
52
52
  // Replace `#` in plural rules with the actual numeric value. Only
@@ -58,7 +58,7 @@ function bindFormatValuesWithBuilder(builder, nodes, locales, formatters, format
58
58
  }
59
59
  continue;
60
60
  }
61
- const variableName = node[intl_ast_1.AstNodeIndices.Value];
61
+ const variableName = node[1 /* AstNodeIndices.Value */];
62
62
  // Enforce that all required values are provided by the caller, even if the
63
63
  // actual value is falsy/undefined.
64
64
  if (!(variableName in values) && !isRichTextTag(variableName)) {
@@ -67,10 +67,7 @@ function bindFormatValuesWithBuilder(builder, nodes, locales, formatters, format
67
67
  const value = values[variableName];
68
68
  switch (nodeType) {
69
69
  case intl_ast_1.FormatJsNodeType.Argument:
70
- // Empty values don't need to be added at all, they are purely for AST representation.
71
- if (variableName == '$_')
72
- break;
73
- if (typeof value === 'object') {
70
+ if (typeof value === 'object' || typeof value === 'function') {
74
71
  builder.pushObject(value);
75
72
  }
76
73
  else {
@@ -80,7 +77,7 @@ function bindFormatValuesWithBuilder(builder, nodes, locales, formatters, format
80
77
  }
81
78
  break;
82
79
  case intl_ast_1.FormatJsNodeType.Date: {
83
- const nodeStyle = node[intl_ast_1.AstNodeIndices.Style];
80
+ const nodeStyle = node[2 /* AstNodeIndices.Style */];
84
81
  // Distinct from FormatJS: We don't currently parse the skeleton ahead of time in the AST,
85
82
  // so this manages parsing the skeleton as well before passing it onto the date formatter.
86
83
  const style = nodeStyle in formatConfig.date
@@ -93,7 +90,7 @@ function bindFormatValuesWithBuilder(builder, nodes, locales, formatters, format
93
90
  break;
94
91
  }
95
92
  case intl_ast_1.FormatJsNodeType.Time: {
96
- const nodeStyle = node[intl_ast_1.AstNodeIndices.Style];
93
+ const nodeStyle = node[2 /* AstNodeIndices.Style */];
97
94
  // Distinct from FormatJS: We don't currently parse the skeleton ahead of time in the AST,
98
95
  // so this manages parsing the skeleton as well before passing it onto the date formatter.
99
96
  const style = nodeStyle in formatConfig.time
@@ -107,7 +104,7 @@ function bindFormatValuesWithBuilder(builder, nodes, locales, formatters, format
107
104
  break;
108
105
  }
109
106
  case intl_ast_1.FormatJsNodeType.Number: {
110
- const nodeStyle = node[intl_ast_1.AstNodeIndices.Style];
107
+ const nodeStyle = node[2 /* AstNodeIndices.Style */];
111
108
  // Distinct from FormatJS: We don't currently parse the skeleton ahead of time in the AST,
112
109
  // so this manages parsing the skeleton as well before passing it onto the date formatter.
113
110
  const style = nodeStyle in formatConfig.number
@@ -122,10 +119,14 @@ function bindFormatValuesWithBuilder(builder, nodes, locales, formatters, format
122
119
  break;
123
120
  }
124
121
  case intl_ast_1.FormatJsNodeType.Tag: {
125
- const children = node[intl_ast_1.AstNodeIndices.Children];
122
+ const children = node[2 /* AstNodeIndices.Children */];
123
+ const control = node[3 /* AstNodeIndices.Control */];
126
124
  const appliedChildren = bindFormatValues(builder.constructor, children, locales, formatters, formatConfig, values, currentPluralValue);
125
+ const appliedControl = control != null
126
+ ? bindFormatValues(builder.constructor, control, locales, formatters, formatConfig, values, currentPluralValue)
127
+ : [];
127
128
  if (isRichTextTag(variableName)) {
128
- builder.pushRichTextTag(variableName, appliedChildren);
129
+ builder.pushRichTextTag(variableName, appliedChildren, appliedControl);
129
130
  }
130
131
  else {
131
132
  if (typeof value !== 'function') {
@@ -145,8 +146,8 @@ function bindFormatValuesWithBuilder(builder, nodes, locales, formatters, format
145
146
  break;
146
147
  }
147
148
  case intl_ast_1.FormatJsNodeType.Select: {
148
- const value = node[intl_ast_1.AstNodeIndices.Value];
149
- const options = node[intl_ast_1.AstNodeIndices.Options];
149
+ const value = node[1 /* AstNodeIndices.Value */];
150
+ const options = node[2 /* AstNodeIndices.Options */];
150
151
  const option = value in options ? options[value] : options.other;
151
152
  if (option == null) {
152
153
  throw `${value} is not a known option for select value ${variableName}. Valid options are ${Object.keys(options).join(', ')}`;
@@ -155,10 +156,10 @@ function bindFormatValuesWithBuilder(builder, nodes, locales, formatters, format
155
156
  break;
156
157
  }
157
158
  case intl_ast_1.FormatJsNodeType.Plural: {
158
- const value = node[intl_ast_1.AstNodeIndices.Value];
159
- const options = node[intl_ast_1.AstNodeIndices.Options];
160
- const offset = node[intl_ast_1.AstNodeIndices.Offset];
161
- const pluralType = node[intl_ast_1.AstNodeIndices.PluralType];
159
+ const value = node[1 /* AstNodeIndices.Value */];
160
+ const options = node[2 /* AstNodeIndices.Options */];
161
+ const offset = node[3 /* AstNodeIndices.Offset */];
162
+ const pluralType = node[4 /* AstNodeIndices.PluralType */];
162
163
  const option = (() => {
163
164
  var _a;
164
165
  const exactSelector = `=${value}`;
@@ -183,6 +184,12 @@ function bindFormatValuesWithBuilder(builder, nodes, locales, formatters, format
183
184
  }
184
185
  function bindFormatValues(Builder, nodes, locales, formatters, formatConfig, values = {}, currentPluralValue) {
185
186
  const builder = new Builder();
186
- bindFormatValuesWithBuilder(builder, nodes, locales, formatters, formatConfig, values, currentPluralValue);
187
- return builder.finish();
187
+ if (typeof nodes === 'string') {
188
+ builder.pushLiteralText(nodes);
189
+ return builder.finish();
190
+ }
191
+ else {
192
+ bindFormatValuesWithBuilder(builder, nodes, locales, formatters, formatConfig, values, currentPluralValue);
193
+ return builder.finish();
194
+ }
188
195
  }
@@ -27,12 +27,11 @@ var RichTextNodeType;
27
27
  RichTextNodeType["Object"] = "object";
28
28
  })(RichTextNodeType || (exports.RichTextNodeType = RichTextNodeType = {}));
29
29
  const AST_RICH_TEXT_ELEMENTS = {
30
- $_: () => ({ type: RichTextNodeType.Text, content: '' }),
31
30
  $b: (content) => ({ type: RichTextNodeType.Strong, content }),
32
31
  $i: (content) => ({ type: RichTextNodeType.Emphasis, content }),
33
32
  $del: (content) => ({ type: RichTextNodeType.Strikethrough, content }),
34
33
  $code: (content) => ({ type: RichTextNodeType.Code, content }),
35
- $link: ([target, ...content]) => ({
34
+ $link: (content, _, [target]) => ({
36
35
  type: RichTextNodeType.Link,
37
36
  target: target.content,
38
37
  content,
@@ -44,11 +43,11 @@ class AstBuilder extends format_1.FormatBuilder {
44
43
  super(...arguments);
45
44
  this.result = [];
46
45
  }
47
- pushRichTextTag(tag, children) {
46
+ pushRichTextTag(tag, children, control) {
48
47
  if (!(tag in AST_RICH_TEXT_ELEMENTS)) {
49
48
  throw `${tag} is not a known rich text formatting tag`;
50
49
  }
51
- const result = AST_RICH_TEXT_ELEMENTS[tag](children, '');
50
+ const result = AST_RICH_TEXT_ELEMENTS[tag](children, '', control);
52
51
  if (Array.isArray(result)) {
53
52
  this.result.push(...result);
54
53
  }
@@ -4,12 +4,11 @@ exports.markdownFormatter = void 0;
4
4
  exports.formatToMarkdownString = formatToMarkdownString;
5
5
  const string_1 = require("./string");
6
6
  const MARKDOWN_RICH_TEXT_ELEMENTS = {
7
- $_: () => '',
8
7
  $b: (content) => '**' + content.join('') + '**',
9
8
  $i: (content) => '*' + content.join('') + '*',
10
9
  $del: (content) => '~~' + content.join('') + '~~',
11
10
  $code: (content) => '`' + content.join('') + '`',
12
- $link: ([target, ...content]) => '[' + content.join('') + '](' + target + ')',
11
+ $link: (content, _, [target]) => '[' + content.join('') + '](' + target + ')',
13
12
  $p: (content) => content.join('') + '\n\n',
14
13
  };
15
14
  class MarkdownBuilder extends string_1.StringBuilder {
@@ -17,8 +16,8 @@ class MarkdownBuilder extends string_1.StringBuilder {
17
16
  super(...arguments);
18
17
  this.result = '';
19
18
  }
20
- pushRichTextTag(tag, children) {
21
- this.result += MARKDOWN_RICH_TEXT_ELEMENTS[tag](children, '');
19
+ pushRichTextTag(tag, children, control) {
20
+ this.result += MARKDOWN_RICH_TEXT_ELEMENTS[tag](children, '', control);
22
21
  }
23
22
  }
24
23
  function formatToMarkdownString(message, values, Builder = MarkdownBuilder) {
@@ -7,13 +7,11 @@ const React = require("react");
7
7
  const format_1 = require("../format");
8
8
  const h = React.createElement;
9
9
  exports.DEFAULT_REACT_RICH_TEXT_ELEMENTS = {
10
- $_: () => '',
11
10
  $b: (content, key) => h('strong', { key }, content),
12
11
  $i: (content, key) => h('em', { key }, content),
13
12
  $del: (content, key) => h('del', { key }, content),
14
13
  $code: (content, key) => h('code', { key }, content),
15
- // $link will always be [href, <empty>, ...content]
16
- $link: ([href, ...content], key) => h('a', { href, key }, content),
14
+ $link: (content, key, [href]) => h('a', { href, key }, content),
17
15
  $p: (content, key) => h('p', { key }, content),
18
16
  };
19
17
  /**
@@ -30,8 +28,8 @@ function createReactBuilder(richTextElements) {
30
28
  this._nodeKey = 0;
31
29
  this.result = [];
32
30
  }
33
- pushRichTextTag(tag, children) {
34
- this.result.push(richTextElements[tag](children, `${this._nodeKey++}`));
31
+ pushRichTextTag(tag, children, control) {
32
+ this.result.push(richTextElements[tag](children, `${this._nodeKey++}`, control));
35
33
  }
36
34
  pushLiteralText(text) {
37
35
  const last = this.result[this.result.length - 1];
@@ -43,8 +41,8 @@ function createReactBuilder(richTextElements) {
43
41
  }
44
42
  }
45
43
  pushObject(value) {
46
- // @ts-expect-error this is technically invalid, but we'll just assume that if a format returns
47
- // an object, it'll be acting as some form of ReactNode.
44
+ // @ts-expect-error this is technically invalid, but we'll just assume that if a format
45
+ // returns an object, it'll be acting as some form of ReactNode.
48
46
  this.result.push(value);
49
47
  }
50
48
  finish() {
@@ -12,7 +12,7 @@ import { IntlManager } from '../intl-manager';
12
12
  export type StringFunctionTypes = FunctionTypes<string>;
13
13
  export declare class StringBuilder extends FormatBuilder<string> {
14
14
  result: string;
15
- pushRichTextTag(tag: RichTextTagNames, children: string[]): void;
15
+ pushRichTextTag(_tag: RichTextTagNames, children: string[], _control: string[]): void;
16
16
  pushLiteralText(text: string): void;
17
17
  pushObject(value: object): void;
18
18
  finish(): string[];
@@ -8,16 +8,14 @@ class StringBuilder extends format_1.FormatBuilder {
8
8
  super(...arguments);
9
9
  this.result = '';
10
10
  }
11
- pushRichTextTag(tag, children) {
11
+ pushRichTextTag(_tag, children, _control) {
12
12
  // Plain string formatting ignores rich text tags and just takes the
13
- // visible content from the children. This means the `target` element
14
- // of a link is ignored, otherwise all the children are just joined
15
- // together directly.
16
- if (tag === '$link') {
17
- this.result += children.slice(1).join('');
18
- return;
13
+ // visible content from the children. This means the control element is not
14
+ // important for string rendering, so the result is always just the
15
+ // children joined together.
16
+ for (const child of children) {
17
+ this.result += child;
19
18
  }
20
- this.result += children.join('');
21
19
  }
22
20
  pushLiteralText(text) {
23
21
  this.result += text;
@@ -68,18 +68,7 @@ class IntlManager {
68
68
  * immediately return the plain string value of the message in the current locale.
69
69
  */
70
70
  string(message) {
71
- const resolved = message(this.currentLocale);
72
- if (resolved == null || resolved.ast.length === 0)
73
- return '';
74
- let result = '';
75
- for (const element of resolved.ast) {
76
- if (typeof element !== 'string') {
77
- throw new Error('Attempted to call `string` formatting on a non-literal message: ' +
78
- JSON.stringify(resolved));
79
- }
80
- result += element;
81
- }
82
- return result;
71
+ return message(this.currentLocale).reserialize();
83
72
  }
84
73
  /**
85
74
  * Return a raw string representing the syntax of the original message, as
@@ -48,7 +48,9 @@ export declare class MessageLoader {
48
48
  /**
49
49
  * Map of pre-parsed messages, keyed by the message key and locale
50
50
  */
51
- _parseCache: Map<string, InternalIntlMessage>;
51
+ _parseCache: Record<LocaleId, {
52
+ [name: string]: InternalIntlMessage;
53
+ }>;
52
54
  /**
53
55
  * List of subscribers listening for changes to the current locale and
54
56
  * when it has loaded.
@@ -22,7 +22,7 @@ class MessageLoader {
22
22
  this.supportedLocales = Object.keys(localeImportMap);
23
23
  this.defaultLocale = defaultLocale;
24
24
  this._localeLoadingPromises = {};
25
- this._parseCache = new Map();
25
+ this._parseCache = {};
26
26
  this._subscribers = new Set();
27
27
  this._loadLocale(this.defaultLocale);
28
28
  this.fallbackMessage = new message_1.InternalIntlMessage([], this.defaultLocale);
@@ -34,7 +34,7 @@ class MessageLoader {
34
34
  // @ts-expect-error `hot` not defined in types.
35
35
  module.hot.accept(file, () => __awaiter(this, void 0, void 0, function* () {
36
36
  yield this._loadLocale(locale);
37
- this._parseCache.clear();
37
+ this._parseCache = {};
38
38
  }));
39
39
  }
40
40
  }
@@ -75,10 +75,11 @@ class MessageLoader {
75
75
  * returning.
76
76
  */
77
77
  getMessageValue(key, locale) {
78
- const cacheKey = key + '@' + locale;
79
- const cachedValue = this._parseCache.get(cacheKey);
80
- if (cachedValue != null)
81
- return cachedValue;
78
+ var _a, _b;
79
+ var _c;
80
+ const parsed = (_a = this._parseCache[locale]) === null || _a === void 0 ? void 0 : _a[key];
81
+ if (parsed)
82
+ return parsed;
82
83
  // Check whether the locale exists in memory. If not, start a request to
83
84
  // load the locale if one has not already started. It's impossible for
84
85
  // the request to finish within the same tick, so return undefined in any
@@ -93,7 +94,7 @@ class MessageLoader {
93
94
  if (key in this.messages[locale]) {
94
95
  const content = this.messages[locale][key];
95
96
  const message = new message_1.InternalIntlMessage(content, locale);
96
- this._parseCache.set(cacheKey, message);
97
+ ((_b = (_c = this._parseCache)[locale]) !== null && _b !== void 0 ? _b : (_c[locale] = {}))[key] = message;
97
98
  return message;
98
99
  }
99
100
  // Otherwise just assume it doesn't exist.
package/dist/message.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { AstNode, FullFormatJsNode } from '@discord/intl-ast';
2
2
  export declare class InternalIntlMessage {
3
3
  locale: string;
4
- ast: AstNode[];
4
+ ast: string | AstNode[];
5
5
  constructor(messageOrAst: AstNode[] | FullFormatJsNode[], locale: string);
6
6
  /**
7
7
  * Return a stringified serialization of this message's AST, with no
package/dist/message.js CHANGED
@@ -12,6 +12,8 @@ class InternalIntlMessage {
12
12
  * formatting or values applied.
13
13
  */
14
14
  reserialize() {
15
+ if (typeof this.ast === 'string')
16
+ return this.ast;
15
17
  const result = { value: '' };
16
18
  serializeAst(this.ast, result);
17
19
  return result.value;
@@ -26,41 +28,38 @@ function serializeAst(ast, result) {
26
28
  result.value += node[0];
27
29
  return;
28
30
  }
29
- switch (node[intl_ast_1.AstNodeIndices.Type]) {
31
+ switch (node[0 /* AstNodeIndices.Type */]) {
30
32
  case intl_ast_1.FormatJsNodeType.Argument:
31
- // Empties are an artifact of our parsing strategy, not necessary here.
32
- if (result.value === '$_')
33
- return;
34
- result.value += '{' + node[intl_ast_1.AstNodeIndices.Value] + '}';
33
+ result.value += '{' + node[1 /* AstNodeIndices.Value */] + '}';
35
34
  return;
36
35
  case intl_ast_1.FormatJsNodeType.Date:
37
- result.value += '{' + node[intl_ast_1.AstNodeIndices.Value] + ', date';
38
- if (node[intl_ast_1.AstNodeIndices.Style] != null) {
39
- result.value += ', ' + node[intl_ast_1.AstNodeIndices.Style];
36
+ result.value += '{' + node[1 /* AstNodeIndices.Value */] + ', date';
37
+ if (node[2 /* AstNodeIndices.Style */] != null) {
38
+ result.value += ', ' + node[2 /* AstNodeIndices.Style */];
40
39
  }
41
40
  result.value += '}';
42
41
  return;
43
42
  case intl_ast_1.FormatJsNodeType.Time:
44
- result.value += '{' + node[intl_ast_1.AstNodeIndices.Value] + ', time';
45
- if (node[intl_ast_1.AstNodeIndices.Style] != null) {
46
- result.value += ', ' + node[intl_ast_1.AstNodeIndices.Style];
43
+ result.value += '{' + node[1 /* AstNodeIndices.Value */] + ', time';
44
+ if (node[2 /* AstNodeIndices.Style */] != null) {
45
+ result.value += ', ' + node[2 /* AstNodeIndices.Style */];
47
46
  }
48
47
  result.value += '}';
49
48
  return;
50
49
  case intl_ast_1.FormatJsNodeType.Number:
51
- result.value += '{' + node[intl_ast_1.AstNodeIndices.Value] + ', number';
52
- if (node[intl_ast_1.AstNodeIndices.Style] != null) {
53
- result.value += ', ' + node[intl_ast_1.AstNodeIndices.Style];
50
+ result.value += '{' + node[1 /* AstNodeIndices.Value */] + ', number';
51
+ if (node[2 /* AstNodeIndices.Style */] != null) {
52
+ result.value += ', ' + node[2 /* AstNodeIndices.Style */];
54
53
  }
55
54
  result.value += '}';
56
55
  return;
57
56
  case intl_ast_1.FormatJsNodeType.Plural: {
58
- const pluralType = node[intl_ast_1.AstNodeIndices.PluralType] == 'ordinal' ? 'selectordinal' : 'plural';
59
- result.value += '{' + node[intl_ast_1.AstNodeIndices.Value] + ', ' + pluralType + ', ';
60
- if (node[intl_ast_1.AstNodeIndices.Offset]) {
61
- result.value += 'offset:' + node[intl_ast_1.AstNodeIndices.Offset];
57
+ const pluralType = node[4 /* AstNodeIndices.PluralType */] == 'ordinal' ? 'selectordinal' : 'plural';
58
+ result.value += '{' + node[1 /* AstNodeIndices.Value */] + ', ' + pluralType + ', ';
59
+ if (node[3 /* AstNodeIndices.Offset */]) {
60
+ result.value += 'offset:' + node[3 /* AstNodeIndices.Offset */];
62
61
  }
63
- for (const [name, arm] of Object.entries(node[intl_ast_1.AstNodeIndices.Options])) {
62
+ for (const [name, arm] of Object.entries(node[2 /* AstNodeIndices.Options */])) {
64
63
  result.value += ' ' + name + ' {';
65
64
  serializeAst(arm, result);
66
65
  result.value += '}';
@@ -71,8 +70,8 @@ function serializeAst(ast, result) {
71
70
  result.value += '#';
72
71
  return;
73
72
  case intl_ast_1.FormatJsNodeType.Select: {
74
- result.value += '{' + node[intl_ast_1.AstNodeIndices.Value] + ', select, ';
75
- for (const [name, arm] of Object.entries(node[intl_ast_1.AstNodeIndices.Options])) {
73
+ result.value += '{' + node[1 /* AstNodeIndices.Value */] + ', select, ';
74
+ for (const [name, arm] of Object.entries(node[2 /* AstNodeIndices.Options */])) {
76
75
  result.value += ' ' + name + ' {';
77
76
  serializeAst(arm, result);
78
77
  result.value += '}';
@@ -86,30 +85,30 @@ function serializeAst(ast, result) {
86
85
  }
87
86
  }
88
87
  function serializeAstTag(node, result) {
89
- switch (node[intl_ast_1.AstNodeIndices.Value]) {
88
+ switch (node[1 /* AstNodeIndices.Value */]) {
90
89
  case '$b':
91
90
  result.value += '**';
92
- serializeAst(node[intl_ast_1.AstNodeIndices.Children], result);
91
+ serializeAst(node[2 /* AstNodeIndices.Children */], result);
93
92
  result.value += '**';
94
93
  return;
95
94
  case '$i':
96
95
  result.value += '*';
97
- serializeAst(node[intl_ast_1.AstNodeIndices.Children], result);
96
+ serializeAst(node[2 /* AstNodeIndices.Children */], result);
98
97
  result.value += '*';
99
98
  return;
100
99
  case '$code':
101
100
  result.value += '`';
102
- serializeAst(node[intl_ast_1.AstNodeIndices.Children], result);
101
+ serializeAst(node[2 /* AstNodeIndices.Children */], result);
103
102
  result.value += '`';
104
103
  return;
105
104
  case '$p':
106
- serializeAst(node[intl_ast_1.AstNodeIndices.Children], result);
105
+ serializeAst(node[2 /* AstNodeIndices.Children */], result);
107
106
  result.value += '\n\n';
108
107
  return;
109
108
  case '$link':
110
109
  // The target is the first child of the link. We don't have to care if it's a placeholder
111
110
  // or not, because the serialization will automatically remove the extra empty.
112
- const [target, ...children] = node[intl_ast_1.AstNodeIndices.Children];
111
+ const [target, ...children] = node[2 /* AstNodeIndices.Children */];
113
112
  result.value += '[';
114
113
  serializeAst(children, result);
115
114
  result.value += '](';
@@ -121,8 +120,8 @@ function serializeAstTag(node, result) {
121
120
  default:
122
121
  // Any other tag name is a hook, which just adds the `$[` on a link tag.
123
122
  result.value += '$[';
124
- serializeAst(node[intl_ast_1.AstNodeIndices.Children], result);
125
- result.value += '](' + node[intl_ast_1.AstNodeIndices.Value] + ')';
123
+ serializeAst(node[2 /* AstNodeIndices.Children */], result);
124
+ result.value += '](' + node[1 /* AstNodeIndices.Value */] + ')';
126
125
  return;
127
126
  }
128
127
  }
package/dist/types.d.ts CHANGED
@@ -193,8 +193,9 @@ export interface FunctionTypeMap {
193
193
  }
194
194
 
195
195
  export interface FunctionTypes<Result, HandlerType = () => void> {
196
- link: undefined | ((content: Result[], key: string) => Result | Result[]);
197
- hook: undefined | ((content: Result[], key: string) => Result | Result[]);
196
+ // TODO: Figure out a nicer way to type `control` here.
197
+ link: undefined | ((content: Result[], key: string, control: Result[]) => Result | Result[]);
198
+ hook: undefined | ((content: Result[], key: string, control: Result[]) => Result | Result[]);
198
199
  handler: undefined | HandlerType;
199
200
  }
200
201
 
@@ -265,7 +266,6 @@ export type RequiredFormatValues<
265
266
  >;
266
267
 
267
268
  export interface RichTextFormattingMap<T = any> {
268
- $_: T;
269
269
  $b: T;
270
270
  $i: T;
271
271
  $p: T;
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "@discord/intl",
3
- "version": "0.9.1",
3
+ "version": "0.10.0-canary.d90454b",
4
4
  "license": "MIT",
5
5
  "description": "Client runtime for managing messages and translations in a React project.",
6
6
  "main": "./dist/index.js",
7
7
  "exports": {
8
8
  ".": {
9
- "default": "./dist/index.js",
10
- "types": "./dist/index.d.ts"
9
+ "types": "./dist/index.d.ts",
10
+ "default": "./dist/index.js"
11
11
  }
12
12
  },
13
13
  "files": [
@@ -19,7 +19,7 @@
19
19
  "@formatjs/intl": "^2.10.1",
20
20
  "@intrnl/xxhash64": "^0.1.2",
21
21
  "intl-messageformat": "^10.5.11",
22
- "@discord/intl-ast": "0.9.1"
22
+ "@discord/intl-ast": "0.10.0-canary.d90454b"
23
23
  },
24
24
  "devDependencies": {
25
25
  "@swc/cli": "^0.3.12",
package/src/format.ts CHANGED
@@ -37,7 +37,7 @@ function isRichTextTag(name: string) {
37
37
  }
38
38
 
39
39
  export abstract class FormatBuilder<Result> {
40
- abstract pushRichTextTag(tag: RichTextTagNames, children: Result[]): void;
40
+ abstract pushRichTextTag(tag: RichTextTagNames, children: Result[], control: Result[]): void;
41
41
  abstract pushLiteralText(text: string): void;
42
42
  abstract pushObject(value: object): void;
43
43
  abstract finish(): Result[];
@@ -89,10 +89,7 @@ export function bindFormatValuesWithBuilder<T, Builder extends FormatBuilder<T>>
89
89
  const value = values[variableName];
90
90
  switch (nodeType) {
91
91
  case FormatJsNodeType.Argument:
92
- // Empty values don't need to be added at all, they are purely for AST representation.
93
- if (variableName == '$_') break;
94
-
95
- if (typeof value === 'object') {
92
+ if (typeof value === 'object' || typeof value === 'function') {
96
93
  builder.pushObject(value);
97
94
  } else {
98
95
  // Taken from FormatJS: non-objects (strings, numbers, and falsy
@@ -149,6 +146,7 @@ export function bindFormatValuesWithBuilder<T, Builder extends FormatBuilder<T>>
149
146
 
150
147
  case FormatJsNodeType.Tag: {
151
148
  const children = node[AstNodeIndices.Children];
149
+ const control = node[AstNodeIndices.Control];
152
150
  const appliedChildren = bindFormatValues(
153
151
  builder.constructor as FormatBuilderConstructor<T>,
154
152
  children,
@@ -158,8 +156,24 @@ export function bindFormatValuesWithBuilder<T, Builder extends FormatBuilder<T>>
158
156
  values,
159
157
  currentPluralValue,
160
158
  );
159
+ const appliedControl =
160
+ control != null
161
+ ? bindFormatValues(
162
+ builder.constructor as FormatBuilderConstructor<T>,
163
+ control,
164
+ locales,
165
+ formatters,
166
+ formatConfig,
167
+ values,
168
+ currentPluralValue,
169
+ )
170
+ : [];
161
171
  if (isRichTextTag(variableName)) {
162
- builder.pushRichTextTag(variableName as RichTextTagNames, appliedChildren);
172
+ builder.pushRichTextTag(
173
+ variableName as RichTextTagNames,
174
+ appliedChildren,
175
+ appliedControl,
176
+ );
163
177
  } else {
164
178
  if (typeof value !== 'function') {
165
179
  throw `expected a function type for a Tag formatting value, ${variableName}. got ${typeof value}: ${value}`;
@@ -224,7 +238,7 @@ export function bindFormatValuesWithBuilder<T, Builder extends FormatBuilder<T>>
224
238
 
225
239
  export function bindFormatValues<Result>(
226
240
  Builder: FormatBuilderConstructor<Result>,
227
- nodes: AstNode[],
241
+ nodes: string | AstNode[],
228
242
  locales: string | string[],
229
243
  formatters: Formatters,
230
244
  formatConfig: FormatConfig,
@@ -232,14 +246,19 @@ export function bindFormatValues<Result>(
232
246
  currentPluralValue?: number,
233
247
  ): Result[] {
234
248
  const builder = new Builder();
235
- bindFormatValuesWithBuilder(
236
- builder,
237
- nodes,
238
- locales,
239
- formatters,
240
- formatConfig,
241
- values,
242
- currentPluralValue,
243
- );
244
- return builder.finish();
249
+ if (typeof nodes === 'string') {
250
+ builder.pushLiteralText(nodes);
251
+ return builder.finish();
252
+ } else {
253
+ bindFormatValuesWithBuilder(
254
+ builder,
255
+ nodes,
256
+ locales,
257
+ formatters,
258
+ formatConfig,
259
+ values,
260
+ currentPluralValue,
261
+ );
262
+ return builder.finish();
263
+ }
245
264
  }
@@ -78,12 +78,11 @@ export type RichTextNode =
78
78
  | RichTextObjectNode;
79
79
 
80
80
  const AST_RICH_TEXT_ELEMENTS: RichTextFormattingMap<AstFunctionTypes['hook']> = {
81
- $_: () => ({ type: RichTextNodeType.Text, content: '' }),
82
81
  $b: (content) => ({ type: RichTextNodeType.Strong, content }),
83
82
  $i: (content) => ({ type: RichTextNodeType.Emphasis, content }),
84
83
  $del: (content) => ({ type: RichTextNodeType.Strikethrough, content }),
85
84
  $code: (content) => ({ type: RichTextNodeType.Code, content }),
86
- $link: ([target, ...content]) => ({
85
+ $link: (content, _, [target]) => ({
87
86
  type: RichTextNodeType.Link,
88
87
  target: target.content as string,
89
88
  content,
@@ -94,11 +93,11 @@ const AST_RICH_TEXT_ELEMENTS: RichTextFormattingMap<AstFunctionTypes['hook']> =
94
93
  class AstBuilder extends FormatBuilder<RichTextNode> {
95
94
  result: RichTextNode[] = [];
96
95
 
97
- pushRichTextTag(tag: RichTextTagNames, children: RichTextNode[]) {
96
+ pushRichTextTag(tag: RichTextTagNames, children: RichTextNode[], control: RichTextNode[]) {
98
97
  if (!(tag in AST_RICH_TEXT_ELEMENTS)) {
99
98
  throw `${tag} is not a known rich text formatting tag`;
100
99
  }
101
- const result = AST_RICH_TEXT_ELEMENTS[tag](children, '');
100
+ const result = AST_RICH_TEXT_ELEMENTS[tag](children, '', control);
102
101
  if (Array.isArray(result)) {
103
102
  this.result.push(...result);
104
103
  } else {
@@ -19,20 +19,19 @@ import { StringBuilder } from './string';
19
19
  export type MarkdownFunctionTypes = FunctionTypes<string>;
20
20
 
21
21
  const MARKDOWN_RICH_TEXT_ELEMENTS: RichTextFormattingMap<MarkdownFunctionTypes['hook']> = {
22
- $_: () => '',
23
22
  $b: (content) => '**' + content.join('') + '**',
24
23
  $i: (content) => '*' + content.join('') + '*',
25
24
  $del: (content) => '~~' + content.join('') + '~~',
26
25
  $code: (content) => '`' + content.join('') + '`',
27
- $link: ([target, ...content]) => '[' + content.join('') + '](' + target + ')',
26
+ $link: (content, _, [target]) => '[' + content.join('') + '](' + target + ')',
28
27
  $p: (content) => content.join('') + '\n\n',
29
28
  };
30
29
 
31
30
  class MarkdownBuilder extends StringBuilder {
32
31
  result: string = '';
33
32
 
34
- pushRichTextTag(tag: RichTextTagNames, children: string[]) {
35
- this.result += MARKDOWN_RICH_TEXT_ELEMENTS[tag](children, '');
33
+ pushRichTextTag(tag: RichTextTagNames, children: string[], control: string[]) {
34
+ this.result += MARKDOWN_RICH_TEXT_ELEMENTS[tag](children, '', control);
36
35
  }
37
36
  }
38
37
 
@@ -27,13 +27,11 @@ export type ReactFunctionTypes = FunctionTypes<
27
27
 
28
28
  const h = React.createElement;
29
29
  export const DEFAULT_REACT_RICH_TEXT_ELEMENTS: RichTextFormattingMap<ReactFunctionTypes['hook']> = {
30
- $_: () => '',
31
30
  $b: (content, key) => h('strong', { key }, content),
32
31
  $i: (content, key) => h('em', { key }, content),
33
32
  $del: (content, key) => h('del', { key }, content),
34
33
  $code: (content, key) => h('code', { key }, content),
35
- // $link will always be [href, <empty>, ...content]
36
- $link: ([href, ...content], key) => h('a', { href, key }, content),
34
+ $link: (content, key, [href]) => h('a', { href, key }, content),
37
35
  $p: (content, key) => h('p', { key }, content),
38
36
  };
39
37
 
@@ -51,8 +49,12 @@ function createReactBuilder(richTextElements: RichTextFormattingMap<ReactFunctio
51
49
  _nodeKey: number = 0;
52
50
  result: React.ReactNode[] = [];
53
51
 
54
- pushRichTextTag(tag: RichTextTagNames, children: React.ReactNode[]) {
55
- this.result.push(richTextElements[tag](children, `${this._nodeKey++}`));
52
+ pushRichTextTag(
53
+ tag: RichTextTagNames,
54
+ children: React.ReactNode[],
55
+ control: React.ReactNode[],
56
+ ) {
57
+ this.result.push(richTextElements[tag](children, `${this._nodeKey++}`, control));
56
58
  }
57
59
 
58
60
  pushLiteralText(text: string) {
@@ -65,8 +67,8 @@ function createReactBuilder(richTextElements: RichTextFormattingMap<ReactFunctio
65
67
  }
66
68
 
67
69
  pushObject(value: object) {
68
- // @ts-expect-error this is technically invalid, but we'll just assume that if a format returns
69
- // an object, it'll be acting as some form of ReactNode.
70
+ // @ts-expect-error this is technically invalid, but we'll just assume that if a format
71
+ // returns an object, it'll be acting as some form of ReactNode.
70
72
  this.result.push(value);
71
73
  }
72
74
 
@@ -15,16 +15,14 @@ export type StringFunctionTypes = FunctionTypes<string>;
15
15
  export class StringBuilder extends FormatBuilder<string> {
16
16
  result: string = '';
17
17
 
18
- pushRichTextTag(tag: RichTextTagNames, children: string[]) {
18
+ pushRichTextTag(_tag: RichTextTagNames, children: string[], _control: string[]) {
19
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;
20
+ // visible content from the children. This means the control element is not
21
+ // important for string rendering, so the result is always just the
22
+ // children joined together.
23
+ for (const child of children) {
24
+ this.result += child;
26
25
  }
27
- this.result += children.join('');
28
26
  }
29
27
 
30
28
  pushLiteralText(text: string) {
@@ -1,4 +1,3 @@
1
- import { LiteralNode } from '@discord/intl-ast';
2
1
  import { createIntl, IntlShape } from '@formatjs/intl';
3
2
  import { Formatters } from 'intl-messageformat';
4
3
 
@@ -117,20 +116,7 @@ export class IntlManager {
117
116
  * immediately return the plain string value of the message in the current locale.
118
117
  */
119
118
  string<T extends TypedIntlMessageGetter<{}>>(message: T): string {
120
- const resolved = message(this.currentLocale);
121
- if (resolved == null || resolved.ast.length === 0) return '';
122
-
123
- let result = '';
124
- for (const element of resolved.ast) {
125
- if (typeof element !== 'string') {
126
- throw new Error(
127
- 'Attempted to call `string` formatting on a non-literal message: ' +
128
- JSON.stringify(resolved),
129
- );
130
- }
131
- result += element;
132
- }
133
- return result;
119
+ return message(this.currentLocale).reserialize();
134
120
  }
135
121
 
136
122
  /**
@@ -54,7 +54,7 @@ export class MessageLoader {
54
54
  /**
55
55
  * Map of pre-parsed messages, keyed by the message key and locale
56
56
  */
57
- _parseCache: Map<string, InternalIntlMessage>;
57
+ _parseCache: Record<LocaleId, { [name: string]: InternalIntlMessage }>;
58
58
 
59
59
  /**
60
60
  * List of subscribers listening for changes to the current locale and
@@ -98,7 +98,7 @@ export class MessageLoader {
98
98
  this.defaultLocale = defaultLocale;
99
99
 
100
100
  this._localeLoadingPromises = {};
101
- this._parseCache = new Map();
101
+ this._parseCache = {};
102
102
  this._subscribers = new Set();
103
103
 
104
104
  this._loadLocale(this.defaultLocale);
@@ -112,7 +112,7 @@ export class MessageLoader {
112
112
  // @ts-expect-error `hot` not defined in types.
113
113
  module.hot.accept(file, async () => {
114
114
  await this._loadLocale(locale);
115
- this._parseCache.clear();
115
+ this._parseCache = {};
116
116
  });
117
117
  }
118
118
  }
@@ -160,9 +160,8 @@ export class MessageLoader {
160
160
  * returning.
161
161
  */
162
162
  getMessageValue(key: string, locale: LocaleId): InternalIntlMessage | undefined {
163
- const cacheKey = key + '@' + locale;
164
- const cachedValue = this._parseCache.get(cacheKey);
165
- if (cachedValue != null) return cachedValue;
163
+ const parsed = this._parseCache[locale]?.[key];
164
+ if (parsed) return parsed;
166
165
 
167
166
  // Check whether the locale exists in memory. If not, start a request to
168
167
  // load the locale if one has not already started. It's impossible for
@@ -179,7 +178,7 @@ export class MessageLoader {
179
178
  if (key in this.messages[locale]) {
180
179
  const content = this.messages[locale][key];
181
180
  const message = new InternalIntlMessage(content, locale);
182
- this._parseCache.set(cacheKey, message);
181
+ (this._parseCache[locale] ??= {})[key] = message;
183
182
  return message;
184
183
  }
185
184
 
package/src/message.ts CHANGED
@@ -10,7 +10,7 @@ import {
10
10
 
11
11
  export class InternalIntlMessage {
12
12
  locale: string;
13
- ast: AstNode[];
13
+ ast: string | AstNode[];
14
14
 
15
15
  constructor(messageOrAst: AstNode[] | FullFormatJsNode[], locale: string) {
16
16
  this.locale = locale;
@@ -22,6 +22,8 @@ export class InternalIntlMessage {
22
22
  * formatting or values applied.
23
23
  */
24
24
  reserialize(): string {
25
+ if (typeof this.ast === 'string') return this.ast;
26
+
25
27
  const result = { value: '' };
26
28
  serializeAst(this.ast, result);
27
29
  return result.value;
@@ -39,8 +41,6 @@ function serializeAst(ast: AstNode[], result: { value: string }) {
39
41
 
40
42
  switch (node[AstNodeIndices.Type]) {
41
43
  case FormatJsNodeType.Argument:
42
- // Empties are an artifact of our parsing strategy, not necessary here.
43
- if (result.value === '$_') return;
44
44
  result.value += '{' + node[AstNodeIndices.Value] + '}';
45
45
  return;
46
46
  case FormatJsNodeType.Date:
package/src/types.d.ts CHANGED
@@ -193,8 +193,9 @@ export interface FunctionTypeMap {
193
193
  }
194
194
 
195
195
  export interface FunctionTypes<Result, HandlerType = () => void> {
196
- link: undefined | ((content: Result[], key: string) => Result | Result[]);
197
- hook: undefined | ((content: Result[], key: string) => Result | Result[]);
196
+ // TODO: Figure out a nicer way to type `control` here.
197
+ link: undefined | ((content: Result[], key: string, control: Result[]) => Result | Result[]);
198
+ hook: undefined | ((content: Result[], key: string, control: Result[]) => Result | Result[]);
198
199
  handler: undefined | HandlerType;
199
200
  }
200
201
 
@@ -265,7 +266,6 @@ export type RequiredFormatValues<
265
266
  >;
266
267
 
267
268
  export interface RichTextFormattingMap<T = any> {
268
- $_: T;
269
269
  $b: T;
270
270
  $i: T;
271
271
  $p: T;