@discord/intl 0.2.0 → 0.2.1-canary.1ed7c43

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/message.js CHANGED
@@ -26,68 +26,161 @@ var __read = (this && this.__read) || function (o, n) {
26
26
  }
27
27
  return ar;
28
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
29
  Object.defineProperty(exports, "__esModule", { value: true });
39
30
  exports.InternalIntlMessage = void 0;
40
31
  var keyless_json_1 = require("./keyless-json");
41
32
  var InternalIntlMessage = /** @class */ (function () {
42
33
  function InternalIntlMessage(messageOrAst, locale) {
43
34
  this.locale = locale;
44
- if (typeof messageOrAst === 'string') {
45
- this.message = messageOrAst;
46
- }
47
- else {
48
- this.ast = messageOrAst;
49
- }
35
+ this.ast = messageOrAst;
50
36
  }
51
- Object.defineProperty(InternalIntlMessage.prototype, "plainAst", {
52
- /**
53
- * A stripped-down representation of the message with no rich formatting
54
- * elements preserved. This will be lazily created the first time it is
55
- * requested (e.g., by calling `formatToPlainString`).
56
- */
57
- get: function () {
58
- return (this._plainAst = this._removeRichTags(this.ast));
59
- },
60
- enumerable: false,
61
- configurable: true
62
- });
63
37
  /**
64
- * Returns the same element with any formatting tags removed. If the element
65
- * itself is a tag, the children will be hoisted and returned, otherwise the
66
- * element is returned as an array of itself.
38
+ * Return a stringified serialization of this message's AST, with no
39
+ * formatting or values applied.
67
40
  */
68
- InternalIntlMessage.prototype._removeRichTags = function (nodes) {
69
- var e_1, _a;
70
- var result = new Array(nodes.length);
71
- try {
72
- for (var nodes_1 = __values(nodes), nodes_1_1 = nodes_1.next(); !nodes_1_1.done; nodes_1_1 = nodes_1.next()) {
73
- var node = nodes_1_1.value;
74
- if (node.type === keyless_json_1.FormatJsNodeType.Tag) {
75
- result.push.apply(result, __spreadArray([], __read(this._removeRichTags(node.children)), false));
41
+ InternalIntlMessage.prototype.reserialize = function () {
42
+ var value = '';
43
+ serializeFormatJsAst(this.ast, { value: value });
44
+ return value;
45
+ };
46
+ return InternalIntlMessage;
47
+ }());
48
+ exports.InternalIntlMessage = InternalIntlMessage;
49
+ // Accepting an object as the `result` parameter lets the same string get passed around and
50
+ // appended, rather than creating a bunch of intermediate strings.
51
+ function serializeFormatJsAst(ast, result) {
52
+ var e_1, _a, e_2, _b, e_3, _c;
53
+ try {
54
+ for (var ast_1 = __values(ast), ast_1_1 = ast_1.next(); !ast_1_1.done; ast_1_1 = ast_1.next()) {
55
+ var node = ast_1_1.value;
56
+ switch (node.type) {
57
+ case keyless_json_1.FormatJsNodeType.Literal:
58
+ result.value += node.value;
59
+ return;
60
+ case keyless_json_1.FormatJsNodeType.Argument:
61
+ // Empties are an artifact of our parsing strategy, not necessary here.
62
+ if (result.value === '$_')
63
+ return;
64
+ result.value += '{' + node.value + '}';
65
+ return;
66
+ case keyless_json_1.FormatJsNodeType.Date:
67
+ result.value += '{' + node.value + ', date';
68
+ if (node.style != null) {
69
+ result.value += ', ' + node.style;
70
+ }
71
+ result.value += '}';
72
+ return;
73
+ case keyless_json_1.FormatJsNodeType.Time:
74
+ result.value += '{' + node.value + ', time';
75
+ if (node.style != null) {
76
+ result.value += ', ' + node.style;
77
+ }
78
+ result.value += '}';
79
+ return;
80
+ case keyless_json_1.FormatJsNodeType.Number:
81
+ result.value += '{' + node.value + ', number';
82
+ if (node.style != null) {
83
+ result.value += ', ' + node.style;
84
+ }
85
+ result.value += '}';
86
+ return;
87
+ case keyless_json_1.FormatJsNodeType.Plural: {
88
+ var pluralType = node.pluralType == 'ordinal' ? 'selectordinal' : 'plural';
89
+ result.value += '{' + node.value + ', ' + pluralType + ', ';
90
+ if (node.offset) {
91
+ result.value += 'offset:' + node.offset;
92
+ }
93
+ try {
94
+ for (var _d = (e_2 = void 0, __values(Object.entries(node.options))), _e = _d.next(); !_e.done; _e = _d.next()) {
95
+ var _f = __read(_e.value, 2), name = _f[0], arm = _f[1];
96
+ result.value += ' ' + name + ' {';
97
+ serializeFormatJsAst(arm.value, result);
98
+ result.value += '}';
99
+ }
100
+ }
101
+ catch (e_2_1) { e_2 = { error: e_2_1 }; }
102
+ finally {
103
+ try {
104
+ if (_e && !_e.done && (_b = _d.return)) _b.call(_d);
105
+ }
106
+ finally { if (e_2) throw e_2.error; }
107
+ }
108
+ return;
76
109
  }
77
- else {
78
- result.push(node);
110
+ case keyless_json_1.FormatJsNodeType.Pound:
111
+ result.value += '#';
112
+ return;
113
+ case keyless_json_1.FormatJsNodeType.Select: {
114
+ result.value += '{' + node.value + ', select, ';
115
+ try {
116
+ for (var _g = (e_3 = void 0, __values(Object.entries(node.options))), _h = _g.next(); !_h.done; _h = _g.next()) {
117
+ var _j = __read(_h.value, 2), name = _j[0], arm = _j[1];
118
+ result.value += ' ' + name + ' {';
119
+ serializeFormatJsAst(arm.value, result);
120
+ result.value += '}';
121
+ }
122
+ }
123
+ catch (e_3_1) { e_3 = { error: e_3_1 }; }
124
+ finally {
125
+ try {
126
+ if (_h && !_h.done && (_c = _g.return)) _c.call(_g);
127
+ }
128
+ finally { if (e_3) throw e_3.error; }
129
+ }
130
+ return;
79
131
  }
132
+ case keyless_json_1.FormatJsNodeType.Tag:
133
+ serializeFormatJsTag(node, result);
134
+ return;
80
135
  }
81
136
  }
82
- catch (e_1_1) { e_1 = { error: e_1_1 }; }
83
- finally {
84
- try {
85
- if (nodes_1_1 && !nodes_1_1.done && (_a = nodes_1.return)) _a.call(nodes_1);
86
- }
87
- finally { if (e_1) throw e_1.error; }
137
+ }
138
+ catch (e_1_1) { e_1 = { error: e_1_1 }; }
139
+ finally {
140
+ try {
141
+ if (ast_1_1 && !ast_1_1.done && (_a = ast_1.return)) _a.call(ast_1);
88
142
  }
89
- return result;
90
- };
91
- return InternalIntlMessage;
92
- }());
93
- exports.InternalIntlMessage = InternalIntlMessage;
143
+ finally { if (e_1) throw e_1.error; }
144
+ }
145
+ }
146
+ function serializeFormatJsTag(node, result) {
147
+ switch (node.value) {
148
+ case '$b':
149
+ result.value += '**';
150
+ serializeFormatJsAst(node.children, result);
151
+ result.value += '**';
152
+ return;
153
+ case '$i':
154
+ result.value += '*';
155
+ serializeFormatJsAst(node.children, result);
156
+ result.value += '*';
157
+ return;
158
+ case '$code':
159
+ result.value += '`';
160
+ serializeFormatJsAst(node.children, result);
161
+ result.value += '`';
162
+ return;
163
+ case '$p':
164
+ serializeFormatJsAst(node.children, result);
165
+ result.value += '\n\n';
166
+ return;
167
+ case '$link':
168
+ // The target is the first child of the link. We don't have to care if it's a placeholder
169
+ // or not, because the serialization will automatically remove the extra empty.
170
+ var _a = __read(node.children), target = _a[0], children = _a.slice(1);
171
+ result.value += '[';
172
+ serializeFormatJsAst(children, result);
173
+ result.value += '](';
174
+ if (target != null) {
175
+ serializeFormatJsAst([target], result);
176
+ }
177
+ result.value += ')';
178
+ return;
179
+ default:
180
+ // Any other tag name is a hook, which just adds the `$[` on a link tag.
181
+ result.value += '$[';
182
+ serializeFormatJsAst(node.children, result);
183
+ result.value += '](' + node.value + ')';
184
+ return;
185
+ }
186
+ }
package/dist/types.d.ts CHANGED
@@ -197,9 +197,9 @@ export interface FunctionTypeMap {
197
197
  handler: any;
198
198
  }
199
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[]);
200
+ export interface FunctionTypes<Result, HandlerType = () => void> {
201
+ link: undefined | ((content: Result[], key: string) => Result | Result[]);
202
+ hook: undefined | ((content: Result[], key: string) => Result | Result[]);
203
203
  handler: undefined | HandlerType;
204
204
  }
205
205
 
@@ -276,6 +276,7 @@ export interface RichTextFormattingMap<T = any> {
276
276
  $p: T;
277
277
  $link: T;
278
278
  $code: T;
279
+ $del: T;
279
280
  }
280
281
 
281
282
  export type RichTextTagNames = keyof RichTextFormattingMap;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@discord/intl",
3
- "version": "0.2.0",
3
+ "version": "0.2.1-canary.1ed7c43",
4
4
  "license": "MIT",
5
5
  "description": "Client runtime for managing messages and translations in a React project.",
6
6
  "main": "./dist/index.js",
@@ -16,6 +16,7 @@
16
16
  ],
17
17
  "dependencies": {
18
18
  "@formatjs/intl": "^2.10.1",
19
+ "@formatjs/icu-skeleton-parser": "1.8.2",
19
20
  "intl-messageformat": "^10.5.11"
20
21
  },
21
22
  "devDependencies": {
package/src/format.ts CHANGED
@@ -20,6 +20,11 @@
20
20
  import { FormatJsNode, FormatJsNodeType } from './keyless-json';
21
21
  import { Formats, Formatters, MissingValueError } from 'intl-messageformat';
22
22
  import { RichTextTagNames } from './types';
23
+ import {
24
+ parseDateTimeSkeleton,
25
+ parseNumberSkeleton,
26
+ parseNumberSkeletonFromString,
27
+ } from '@formatjs/icu-skeleton-parser';
23
28
 
24
29
  /**
25
30
  * Returns true if the tag name should be considered a rich text tag that
@@ -96,10 +101,12 @@ export function bindFormatValuesWithBuilder<T, Builder extends FormatBuilder<T>>
96
101
  case FormatJsNodeType.Date: {
97
102
  // Distinct from FormatJS: We don't currently parse the skeleton ahead of time in the AST,
98
103
  // so this manages parsing the skeleton as well before passing it onto the date formatter.
99
- if (typeof node.style !== 'string') {
100
- throw '@discord/intl does not currently support non-string format skeletons';
101
- }
102
- const style = node.style in formats.date ? formats.date[node.style] : undefined; // TODO: parseSkeleton();
104
+ const style =
105
+ node.style in formats.date
106
+ ? formats.date[node.style]
107
+ : node.style != null
108
+ ? parseDateTimeSkeleton(node.style)
109
+ : undefined;
103
110
  // @ts-expect-error Cast string values to dates properly.
104
111
  builder.pushLiteralText(formatters.getDateTimeFormat(locales, style).format(value));
105
112
  break;
@@ -107,10 +114,12 @@ export function bindFormatValuesWithBuilder<T, Builder extends FormatBuilder<T>>
107
114
  case FormatJsNodeType.Time: {
108
115
  // Distinct from FormatJS: We don't currently parse the skeleton ahead of time in the AST,
109
116
  // so this manages parsing the skeleton as well before passing it onto the date formatter.
110
- if (typeof node.style !== 'string') {
111
- throw '@discord/intl does not currently support non-string format skeletons';
112
- }
113
- const style = node.style in formats.time ? formats.time[node.style] : undefined; // TODO: parseSkeleton();
117
+ const style =
118
+ node.style in formats.time
119
+ ? formats.time[node.style]
120
+ : node.style != null
121
+ ? parseDateTimeSkeleton(node.style)
122
+ : undefined; // TODO: parseSkeleton();
114
123
  builder.pushLiteralText(
115
124
  // @ts-expect-error Cast string values to dates properly.
116
125
  formatters.getDateTimeFormat(locales, style).format(value),
@@ -120,14 +129,14 @@ export function bindFormatValuesWithBuilder<T, Builder extends FormatBuilder<T>>
120
129
  case FormatJsNodeType.Number: {
121
130
  // Distinct from FormatJS: We don't currently parse the skeleton ahead of time in the AST,
122
131
  // so this manages parsing the skeleton as well before passing it onto the date formatter.
123
- if (typeof node.style !== 'string') {
124
- throw '@discord/intl does not currently support non-string format skeletons';
125
- }
126
- const style = node.style in formats.number ? formats.number[node.style] : undefined; // TODO: parseSkeleton();
127
-
132
+ const style =
133
+ node.style in formats.number
134
+ ? formats.number[node.style]
135
+ : node.style != null
136
+ ? parseNumberSkeleton(parseNumberSkeletonFromString(node.style))
137
+ : undefined;
128
138
  // @ts-expect-error Support `scale` style property.
129
- const scaledValue = value as number;
130
- // const scaledValue = (value as number) * (style?.scale ?? 1);
139
+ const scaledValue = style?.scale != 1 ? (value as number) * style.scale : (value as number);
131
140
  builder.pushLiteralText(formatters.getNumberFormat(locales, style).format(scaledValue));
132
141
  break;
133
142
  }
@@ -12,7 +12,7 @@ import { FormatBuilder } from '../format';
12
12
  * Types for formatting functions when calling `formatToParts`, ensuring the
13
13
  * functions yield value AST nodes.
14
14
  */
15
- export type AstFunctionTypes = FunctionTypes<RichTextNode>;
15
+ export type AstFunctionTypes = FunctionTypes<RichTextNode, object>;
16
16
 
17
17
  // This structure aims to match `simple-markdown`'s AST, but that type is very
18
18
  // loosely defined as just "type" and "arbitrary map of content". This is more
@@ -21,6 +21,7 @@ export enum RichTextNodeType {
21
21
  Text = 'text',
22
22
  Strong = 'strong',
23
23
  Emphasis = 'emphasis',
24
+ Strikethrough = 'strikethrough',
24
25
  Code = 'code',
25
26
  Link = 'link',
26
27
  Paragraph = 'paragraph',
@@ -36,15 +37,6 @@ export enum RichTextNodeType {
36
37
  Object = 'object',
37
38
  }
38
39
 
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
40
  interface RichTextNodeBase<ContentType> {
49
41
  type: RichTextNodeType;
50
42
  content: ContentType;
@@ -58,6 +50,9 @@ interface RichTextStrongNode extends RichTextNodeBase<RichTextNode[]> {
58
50
  interface RichTextEmphasisNode extends RichTextNodeBase<RichTextNode[]> {
59
51
  type: RichTextNodeType.Emphasis;
60
52
  }
53
+ interface RichTextStrikethroughNode extends RichTextNodeBase<RichTextNode[]> {
54
+ type: RichTextNodeType.Strikethrough;
55
+ }
61
56
  interface RichTextCodeNode extends RichTextNodeBase<RichTextNode[]> {
62
57
  type: RichTextNodeType.Code;
63
58
  }
@@ -76,6 +71,7 @@ export type RichTextNode =
76
71
  | RichTextTextNode
77
72
  | RichTextStrongNode
78
73
  | RichTextEmphasisNode
74
+ | RichTextStrikethroughNode
79
75
  | RichTextCodeNode
80
76
  | RichTextParagraphNode
81
77
  | RichTextLinkNode
@@ -84,8 +80,9 @@ export type RichTextNode =
84
80
  const AST_RICH_TEXT_ELEMENTS: RichTextFormattingMap<AstFunctionTypes['hook']> = {
85
81
  $_: () => ({ type: RichTextNodeType.Text, content: '' }),
86
82
  $b: (content) => ({ type: RichTextNodeType.Strong, content }),
87
- $code: (content) => ({ type: RichTextNodeType.Code, content }),
88
83
  $i: (content) => ({ type: RichTextNodeType.Emphasis, content }),
84
+ $del: (content) => ({ type: RichTextNodeType.Strikethrough, content }),
85
+ $code: (content) => ({ type: RichTextNodeType.Code, content }),
89
86
  $link: ([target, ...content]) => ({
90
87
  type: RichTextNodeType.Link,
91
88
  target: target.content as string,
@@ -101,7 +98,7 @@ class AstBuilder extends FormatBuilder<RichTextNode> {
101
98
  if (!(tag in AST_RICH_TEXT_ELEMENTS)) {
102
99
  throw `${tag} is not a known rich text formatting tag`;
103
100
  }
104
- const result = AST_RICH_TEXT_ELEMENTS[tag](children);
101
+ const result = AST_RICH_TEXT_ELEMENTS[tag](children, '');
105
102
  if (Array.isArray(result)) {
106
103
  this.result.push(...result);
107
104
  } else {
@@ -21,8 +21,9 @@ export type MarkdownFunctionTypes = FunctionTypes<string>;
21
21
  const MARKDOWN_RICH_TEXT_ELEMENTS: RichTextFormattingMap<MarkdownFunctionTypes['hook']> = {
22
22
  $_: () => '',
23
23
  $b: (content) => '**' + content.join('') + '**',
24
- $code: (content) => '`' + content.join('') + '`',
25
24
  $i: (content) => '*' + content.join('') + '*',
25
+ $del: (content) => '~~' + content.join('') + '~~',
26
+ $code: (content) => '`' + content.join('') + '`',
26
27
  $link: ([target, ...content]) => '[' + content.join('') + '](' + target + ')',
27
28
  $p: (content) => content.join('') + '\n\n',
28
29
  };
@@ -31,7 +32,7 @@ class MarkdownBuilder extends StringBuilder {
31
32
  result: string = '';
32
33
 
33
34
  pushRichTextTag(tag: RichTextTagNames, children: string[]) {
34
- this.result += MARKDOWN_RICH_TEXT_ELEMENTS[tag](children);
35
+ this.result += MARKDOWN_RICH_TEXT_ELEMENTS[tag](children, '');
35
36
  }
36
37
  }
37
38
 
@@ -28,12 +28,13 @@ export type ReactFunctionTypes = FunctionTypes<
28
28
  const h = React.createElement;
29
29
  export const DEFAULT_REACT_RICH_TEXT_ELEMENTS: RichTextFormattingMap<ReactFunctionTypes['hook']> = {
30
30
  $_: () => '',
31
- $b: (content) => h('strong', undefined, content),
32
- $code: (content) => h('code', undefined, content),
33
- $i: (content) => h('em', undefined, content),
31
+ $b: (content, key) => h('strong', { key }, content),
32
+ $i: (content, key) => h('em', { key }, content),
33
+ $del: (content, key) => h('del', { key }, content),
34
+ $code: (content, key) => h('code', { key }, content),
34
35
  // $link will always be [href, <empty>, ...content]
35
- $link: ([href, ...content]) => h('a', { href }, content),
36
- $p: (content) => h('p', undefined, content),
36
+ $link: ([href, ...content], key) => h('a', { href, key }, content),
37
+ $p: (content, key) => h('p', { key }, content),
37
38
  };
38
39
 
39
40
  /**
@@ -47,10 +48,11 @@ function createReactBuilder(richTextElements: RichTextFormattingMap<ReactFunctio
47
48
  new (): FormatBuilder<React.ReactNode>;
48
49
  } {
49
50
  return class extends FormatBuilder<React.ReactNode> {
51
+ _nodeKey: number = 0;
50
52
  result: React.ReactNode[] = [];
51
53
 
52
54
  pushRichTextTag(tag: RichTextTagNames, children: React.ReactNode[]) {
53
- this.result.push(richTextElements[tag](children));
55
+ this.result.push(richTextElements[tag](children, `${this._nodeKey++}`));
54
56
  }
55
57
 
56
58
  pushLiteralText(text: string) {
@@ -69,18 +71,23 @@ function createReactBuilder(richTextElements: RichTextFormattingMap<ReactFunctio
69
71
  };
70
72
  }
71
73
 
74
+ export type ReactIntlMessage = React.ReactElement<
75
+ {},
76
+ typeof React.Fragment & { __brand: 'discord-intl' }
77
+ >;
78
+
72
79
  export function formatReact(
73
80
  this: IntlManager,
74
81
  message: AnyIntlMessage,
75
82
  values: object,
76
83
  Builder: FormatBuilderConstructor<React.ReactElement>,
77
- ): React.ReactElement {
84
+ ): ReactIntlMessage {
78
85
  if (typeof message === 'string') {
79
- return React.createElement(React.Fragment, undefined, message);
86
+ return React.createElement(React.Fragment, undefined, message) as ReactIntlMessage;
80
87
  }
81
88
 
82
89
  const parts = this.bindFormatValues(Builder, message, values);
83
- return React.createElement(React.Fragment, undefined, parts);
90
+ return React.createElement(React.Fragment, undefined, parts) as ReactIntlMessage;
84
91
  }
85
92
 
86
93
  /**
@@ -90,7 +97,7 @@ export function formatReact(
90
97
  */
91
98
  export function makeReactFormatter(
92
99
  richTextElements: RichTextFormattingMap<ReactFunctionTypes['hook']>,
93
- ): FormatterImplementation<ReactFunctionTypes, React.ReactElement, React.ReactNode> {
100
+ ): FormatterImplementation<ReactFunctionTypes, ReactIntlMessage, React.ReactNode> {
94
101
  return {
95
102
  format: formatReact,
96
103
  builder: createReactBuilder(richTextElements),
package/src/index.ts CHANGED
@@ -1,9 +1,14 @@
1
1
  import * as React from 'react';
2
2
  import { RichTextNode } from './formatters';
3
3
 
4
- export { IntlManager, createLoader, DEFAULT_LOCALE, type FormatFunction } from './intl-manager';
5
4
  export * from './formatters';
6
-
5
+ export { IntlManager, DEFAULT_LOCALE, type FormatFunction } from './intl-manager';
6
+ export {
7
+ createLoader,
8
+ loadAllMessagesInLocale,
9
+ waitForAllDefaultIntlMessagesLoaded,
10
+ MessageLoader,
11
+ } from './message-loader';
7
12
  export type * from './types.d.ts';
8
13
 
9
14
  /**
@@ -21,6 +26,7 @@ export type * from './types.d.ts';
21
26
  * intl message, even if the actual value comes from elsewhere (like a
22
27
  * user-generated string).
23
28
  */
29
+ // TODO: Replace this with ReactIntlMessage from `formatters/react`.
24
30
  export type ReactIntlMessage = string | React.ReactElement;
25
31
 
26
32
  /**
@@ -1,8 +1,6 @@
1
1
  import { createIntl, IntlShape } from '@formatjs/intl';
2
2
  import { Formats, Formatters, IntlMessageFormat } from 'intl-messageformat';
3
3
 
4
- import { LocaleId, LocaleImportMap, MessageLoader } from './message-loader';
5
-
6
4
  import type {
7
5
  FormatterImplementation,
8
6
  IntlMessageGetter,
@@ -11,6 +9,7 @@ import type {
11
9
  } from './types';
12
10
  import { InternalIntlMessage } from './message';
13
11
  import { bindFormatValues, FormatBuilderConstructor } from './format';
12
+ import { FormatJsLiteral } from './keyless-json';
14
13
 
15
14
  /**
16
15
  * Fallback locale used for all internationalization when an operation in the
@@ -18,8 +17,7 @@ import { bindFormatValues, FormatBuilderConstructor } from './format';
18
17
  */
19
18
  export const DEFAULT_LOCALE: string = 'en-US';
20
19
 
21
- type FormatterReturnType<F extends FormatterImplementation<any, any>> =
22
- F extends FormatterImplementation<any, infer Return> ? Return : never;
20
+ type FormatterReturnType<F extends FormatterImplementation<any, any>> = ReturnType<F['format']>;
23
21
 
24
22
  type FormatterFunctionTypes<F extends FormatterImplementation<any, any>> =
25
23
  F extends FormatterImplementation<infer Functions, any> ? Functions : never;
@@ -116,8 +114,26 @@ export class IntlManager {
116
114
  * immediately return the plain string value of the message in the current locale.
117
115
  */
118
116
  string<T extends TypedIntlMessageGetter<undefined>>(message: T): string {
119
- // TODO: Figure out how to make this typing exact.
120
- return message(this.currentLocale).message;
117
+ const resolved = message(this.currentLocale);
118
+ if (resolved == null || resolved.ast.length === 0) return '';
119
+
120
+ // TODO: Figure out how to make this typing exact. This currently relies on
121
+ // the generic typing being sound enough to know that the message can only
122
+ // contain a single static text node and no placeholders or rich text.
123
+ return (resolved.ast[0] as FormatJsLiteral).value;
124
+ }
125
+
126
+ /**
127
+ * Return a raw string representing the syntax of the original message, as
128
+ * authored, with no values replaced. The result of this function could be
129
+ * written back to the definition file for the message and re-parsed to
130
+ * create an identical message to the original.
131
+ *
132
+ * This should rarely be necessary outside of sending raw messages to other
133
+ * applications that do their own message parsing.
134
+ */
135
+ reserialize<T extends IntlMessageGetter>(message: T): string {
136
+ return message(this.currentLocale)[0].reserialize();
121
137
  }
122
138
 
123
139
  bindFormatValues<T>(
@@ -135,16 +151,3 @@ export class IntlManager {
135
151
  );
136
152
  }
137
153
  }
138
-
139
- /**
140
- * Create a new MessageLoader, which handles lazily loading messages for
141
- * different locales and sanity checks as needed to provide accessors for each
142
- * message defined in `messageKeys`.
143
- */
144
- export function createLoader(
145
- messageKeys: string[],
146
- localeImportMap: LocaleImportMap,
147
- defaultLocale: LocaleId,
148
- ) {
149
- return new MessageLoader(messageKeys, localeImportMap, defaultLocale);
150
- }