@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/message.d.ts CHANGED
@@ -1,41 +1,20 @@
1
- import { Formats, Formatters, MessageFormatPart } from 'intl-messageformat';
2
- import type { MessageFormatElement } from '@formatjs/icu-messageformat-parser';
1
+ import { FormatJsNode } from './keyless-json';
3
2
  export declare class InternalIntlMessage {
4
3
  locale: string;
5
4
  message?: string;
6
- ast: MessageFormatElement[];
5
+ ast: FormatJsNode[];
7
6
  /**
8
7
  * A stripped-down representation of the message with no rich formatting
9
8
  * elements preserved. This will be lazily created the first time it is
10
9
  * requested (e.g., by calling `formatToPlainString`).
11
10
  */
12
- plainAst?: MessageFormatElement[];
13
- constructor(messageOrAst: string | MessageFormatElement[], locale: string);
14
- /**
15
- * Format this message with the given locale into an AST of chunks that can
16
- * then be used or rendered into strings, React elements, or anything else as
17
- * needed.
18
- */
19
- formatToParts<T>(formatters: Formatters, formats: Formats, values?: Record<string, any>): Array<MessageFormatPart<T>>;
20
- /**
21
- * Formats this message just like `formatToParts`, but strips all stylistic
22
- * elements and tags and returns a plain string with only the literal text of
23
- * the message remaining.
24
- *
25
- * This method works by quickly scanning the string and removing any tag
26
- * elements, formatting, then concatenating the pieces into a single result.
27
- *
28
- * Note that this function does _not_ handle serializing values other than
29
- * plain/primitive values into strings. All values are simply converted using
30
- * the `String()` constructor. So, for example, passing a function as a value
31
- * will directly serialize the function definition into a string, rather than
32
- * calling the function or evaluating the result.
33
- */
34
- formatToPlainString(formatters: Formatters, formats: Formats, values?: Record<string, any>): string;
11
+ get plainAst(): FormatJsNode[];
12
+ _plainAst: FormatJsNode[];
13
+ constructor(messageOrAst: string | FormatJsNode[], locale: string);
35
14
  /**
36
15
  * Returns the same element with any formatting tags removed. If the element
37
16
  * itself is a tag, the children will be hoisted and returned, otherwise the
38
17
  * element is returned as an array of itself.
39
18
  */
40
- _removeRichTags(element: MessageFormatElement): MessageFormatElement[];
19
+ private _removeRichTags;
41
20
  }
package/dist/message.js CHANGED
@@ -37,106 +37,57 @@ var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) {
37
37
  };
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
39
  exports.InternalIntlMessage = void 0;
40
- var icu_messageformat_parser_1 = require("@formatjs/icu-messageformat-parser");
41
- var intl_messageformat_1 = require("intl-messageformat");
40
+ var keyless_json_1 = require("./keyless-json");
42
41
  var InternalIntlMessage = /** @class */ (function () {
43
42
  function InternalIntlMessage(messageOrAst, locale) {
44
43
  this.locale = locale;
45
44
  if (typeof messageOrAst === 'string') {
46
45
  this.message = messageOrAst;
47
- this.ast = (0, icu_messageformat_parser_1.parse)(messageOrAst);
48
46
  }
49
47
  else {
50
48
  this.ast = messageOrAst;
51
49
  }
52
- this.plainAst = undefined;
53
50
  }
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
+ });
54
63
  /**
55
- * Format this message with the given locale into an AST of chunks that can
56
- * then be used or rendered into strings, React elements, or anything else as
57
- * needed.
58
- */
59
- InternalIntlMessage.prototype.formatToParts = function (formatters, formats, values) {
60
- return (0, intl_messageformat_1.formatToParts)(this.ast, this.locale, formatters, formats, values);
61
- };
62
- /**
63
- * Formats this message just like `formatToParts`, but strips all stylistic
64
- * elements and tags and returns a plain string with only the literal text of
65
- * the message remaining.
66
- *
67
- * This method works by quickly scanning the string and removing any tag
68
- * elements, formatting, then concatenating the pieces into a single result.
69
- *
70
- * Note that this function does _not_ handle serializing values other than
71
- * plain/primitive values into strings. All values are simply converted using
72
- * the `String()` constructor. So, for example, passing a function as a value
73
- * will directly serialize the function definition into a string, rather than
74
- * calling the function or evaluating the result.
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.
75
67
  */
76
- InternalIntlMessage.prototype.formatToPlainString = function (formatters, formats, values) {
77
- var e_1, _a, _b, e_2, _c;
78
- // Lazily create the reduced AST if it hasn't already been done.
79
- if (this.plainAst == null) {
80
- this.plainAst = [];
81
- try {
82
- for (var _d = __values(this.ast), _e = _d.next(); !_e.done; _e = _d.next()) {
83
- var part = _e.value;
84
- (_b = this.plainAst).push.apply(_b, __spreadArray([], __read(this._removeRichTags(part)), false));
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));
85
76
  }
86
- }
87
- catch (e_1_1) { e_1 = { error: e_1_1 }; }
88
- finally {
89
- try {
90
- if (_e && !_e.done && (_a = _d.return)) _a.call(_d);
77
+ else {
78
+ result.push(node);
91
79
  }
92
- finally { if (e_1) throw e_1.error; }
93
- }
94
- }
95
- var parts = (0, intl_messageformat_1.formatToParts)(this.plainAst, this.locale, formatters, formats, values);
96
- var result = '';
97
- try {
98
- for (var parts_1 = __values(parts), parts_1_1 = parts_1.next(); !parts_1_1.done; parts_1_1 = parts_1.next()) {
99
- var part = parts_1_1.value;
100
- result += String(part.value);
101
80
  }
102
81
  }
103
- catch (e_2_1) { e_2 = { error: e_2_1 }; }
82
+ catch (e_1_1) { e_1 = { error: e_1_1 }; }
104
83
  finally {
105
84
  try {
106
- if (parts_1_1 && !parts_1_1.done && (_c = parts_1.return)) _c.call(parts_1);
85
+ if (nodes_1_1 && !nodes_1_1.done && (_a = nodes_1.return)) _a.call(nodes_1);
107
86
  }
108
- finally { if (e_2) throw e_2.error; }
87
+ finally { if (e_1) throw e_1.error; }
109
88
  }
110
89
  return result;
111
90
  };
112
- /**
113
- * Returns the same element with any formatting tags removed. If the element
114
- * itself is a tag, the children will be hoisted and returned, otherwise the
115
- * element is returned as an array of itself.
116
- */
117
- InternalIntlMessage.prototype._removeRichTags = function (element) {
118
- var e_3, _a;
119
- if (element.type === icu_messageformat_parser_1.TYPE.tag) {
120
- var childContent = [];
121
- try {
122
- for (var _b = __values(element.children), _c = _b.next(); !_c.done; _c = _b.next()) {
123
- var child = _c.value;
124
- childContent.push.apply(childContent, __spreadArray([], __read(this._removeRichTags(child)), false));
125
- }
126
- }
127
- catch (e_3_1) { e_3 = { error: e_3_1 }; }
128
- finally {
129
- try {
130
- if (_c && !_c.done && (_a = _b.return)) _a.call(_b);
131
- }
132
- finally { if (e_3) throw e_3.error; }
133
- }
134
- return childContent;
135
- }
136
- else {
137
- return [element];
138
- }
139
- };
140
91
  return InternalIntlMessage;
141
92
  }());
142
93
  exports.InternalIntlMessage = InternalIntlMessage;
package/dist/types.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import type { InternalIntlMessage } from './message';
2
2
  import type { IntlMessageGetterAdditions } from './message-loader';
3
+ import { FormatBuilderConstructor } from './format';
3
4
 
4
5
  /**
5
6
  * Use this function to define messages as part of the `@discord/intl`
@@ -11,14 +12,6 @@ import type { IntlMessageGetterAdditions } from './message-loader';
11
12
  */
12
13
  export declare function defineMessages(messages: object): object;
13
14
 
14
- /**
15
- * A map of tag names to render functions to apply when the tags are
16
- * encountered while formatting a message. Each `IntlManager` can be
17
- * constructed with a different map of these elements, for example to allow
18
- * one manager to render React output while another renders accessibility text.
19
- */
20
- export type RichTextElementMap = Record<string, (chunks: any[]) => any>;
21
-
22
15
  /**
23
16
  * Placeholder type representing that this property is a Hook to be rendered
24
17
  * while formatting.
@@ -46,19 +39,8 @@ export type IntlTime = string | number | Date | null | undefined;
46
39
  * strings, and are able to skip multiple formatting steps to work more
47
40
  * efficiently. This type represents those messages distinctly.
48
41
  */
49
- type PlainIntlMessage = string;
50
- /**
51
- * A richly-formatted message that injects dynamic values while formatting,
52
- * with the types of those values defined by the `FormatValues` object. All
53
- * messages of this type must have at least one value in the object, otherwise
54
- * the message should be typed as a `PlainIntlMessage`.
55
- */
56
- type TypedIntlMessage<FormatValues extends object> = InternalIntlMessage & {
57
- __values: FormatValues;
58
- };
59
-
60
- type AnyIntlMessage<FormatValues extends object | undefined = undefined> =
61
- FormatValues extends undefined ? PlainIntlMessage : TypedIntlMessage<FormatValues>;
42
+ export type PlainIntlMessage = string;
43
+ export type AnyIntlMessage = PlainIntlMessage | InternalIntlMessage;
62
44
 
63
45
  export interface IntlMessageGetter extends IntlMessageGetterAdditions {
64
46
  (locale: string): InternalIntlMessage | PlainIntlMessage;
@@ -85,8 +67,8 @@ export interface TypedIntlMessageGetter<FormatValues extends object | undefined>
85
67
  // from the declared type. The latter of these is definitely semantically
86
68
  // correct, but the former is more just an annoying part of the type system
87
69
  // because of this returned type being nested within this interface and the
88
- // strict subtying that TypeScript uses.
89
- (locale: string): InternalIntlMessage | PlainIntlMessage;
70
+ // strict subtyping that TypeScript uses.
71
+ (locale: string): InternalIntlMessage;
90
72
 
91
73
  /**
92
74
  * A phantom type (not part of the actual typing of the object) that ensures
@@ -183,7 +165,7 @@ type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (
183
165
  * checked against the provided message types, and error messages will be
184
166
  * accurate as a result.
185
167
  */
186
- type FormatValuesFor<T> =
168
+ export type FormatValuesFor<T> =
187
169
  // `[]` syntax forces TypeScript to not distribute the possible union of
188
170
  // types, meaning this represents "_all_ values of T are an extension of
189
171
  // the parent type", which in this case represents messages with no
@@ -209,12 +191,18 @@ type FormatValuesFor<T> =
209
191
  * the caller to inject React elements within a string, while an AST formatter
210
192
  * might require a specific object shape, or allow any plain object.
211
193
  */
212
- interface FunctionTypeMap {
194
+ export interface FunctionTypeMap {
213
195
  link: any;
214
196
  hook: any;
215
197
  handler: any;
216
198
  }
217
199
 
200
+ export interface FunctionTypes<Result, HandlerType = (content: Result[]) => Result | Result[]> {
201
+ link: undefined | ((content: Result[]) => Result | Result[]);
202
+ hook: undefined | ((content: Result[]) => Result | Result[]);
203
+ handler: undefined | HandlerType;
204
+ }
205
+
218
206
  type ReplaceKeepingNullability<T, P> = P | Exclude<T, NonNullable<T>>;
219
207
 
220
208
  /**
@@ -226,6 +214,8 @@ type ReplaceKeepingNullability<T, P> = P | Exclude<T, NonNullable<T>>;
226
214
  type MapFunctionTypes<T, FunctionTypes extends FunctionTypeMap> = {
227
215
  [K in keyof T]: ReplaceKeepingNullability<
228
216
  T[K],
217
+ // TODO: replace these types with string literal types in the generated
218
+ // file, turning this whole chain into `FunctionTypes[T[K]] ?? T[K]`.
229
219
  NonNullable<T[K]> extends LinkFunction
230
220
  ? FunctionTypes['link']
231
221
  : NonNullable<T[K]> extends HookFunction
@@ -251,7 +241,7 @@ type CheckStrictAny<Check, Yes, No> = (Check extends never ? true : false) exten
251
241
  * // Result is `{name: string}`
252
242
  * ```
253
243
  */
254
- type IntlMessageGetterInnerType<T extends IntlMessageGetter> =
244
+ export type IntlMessageGetterInnerType<T extends IntlMessageGetter> =
255
245
  T extends TypedIntlMessageGetter<infer U> ? U : never;
256
246
 
257
247
  /**
@@ -260,13 +250,12 @@ type IntlMessageGetterInnerType<T extends IntlMessageGetter> =
260
250
  * types from `FunctionTypes`. The result is the set of required values
261
251
  * necessary to format the message of type `T` with the calling formatter.
262
252
  */
263
- type RequiredFormatValues<
253
+ export type RequiredFormatValues<
264
254
  Values extends IntlMessageGetter,
265
- DefaultElements,
266
255
  FunctionTypes extends FunctionTypeMap,
267
256
  > =
268
257
  IntlMessageGetterInnerType<Values> extends undefined
269
- ? never
258
+ ? {}
270
259
  : CheckStrictAny<
271
260
  IntlMessageGetterInnerType<Values>,
272
261
  // If `T` is explicitly `any`, then individual properties can't be typed and
@@ -277,23 +266,30 @@ type RequiredFormatValues<
277
266
  // value and turn every property into the mapped `LinkFunction` type, which
278
267
  // is almost always going to be wrong.
279
268
  any,
280
- MapFunctionTypes<
281
- Omit<FormatValuesFor<IntlMessageGetterInnerType<Values>>, keyof DefaultElements>,
282
- FunctionTypes
283
- >
269
+ MapFunctionTypes<FormatValuesFor<IntlMessageGetterInnerType<Values>>, FunctionTypes>
284
270
  >;
285
271
 
286
- export interface IntlOptionalRichTextKeys {
287
- $_: any;
288
- $b: any;
289
- $i: any;
290
- $p: any;
291
- $link: any;
292
- $code: any;
272
+ export interface RichTextFormattingMap<T = any> {
273
+ $_: T;
274
+ $b: T;
275
+ $i: T;
276
+ $p: T;
277
+ $link: T;
278
+ $code: T;
293
279
  }
294
280
 
295
- export type IntlFormatFunctionValuesArg<T extends IntlMessageGetter> = RequiredFormatValues<
296
- T,
297
- IntlOptionalRichTextKeys,
298
- FunctionTypeMap
299
- >;
281
+ export type RichTextTagNames = keyof RichTextFormattingMap;
282
+
283
+ export interface FormatterImplementation<
284
+ FunctionTypes extends FunctionTypeMap,
285
+ Result,
286
+ BuilderResult = Result,
287
+ > {
288
+ format: (
289
+ message: AnyIntlMessage,
290
+ values: object,
291
+ builder?: FormatBuilderConstructor<BuilderResult>,
292
+ ) => Result;
293
+ builder: FormatBuilderConstructor<BuilderResult extends (infer T)[] ? T : BuilderResult>;
294
+ __$functionTypes?: FunctionTypes;
295
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@discord/intl",
3
- "version": "0.1.3-rc.0",
3
+ "version": "0.2.0",
4
4
  "license": "MIT",
5
5
  "description": "Client runtime for managing messages and translations in a React project.",
6
6
  "main": "./dist/index.js",
@@ -15,7 +15,6 @@
15
15
  "src"
16
16
  ],
17
17
  "dependencies": {
18
- "@formatjs/icu-messageformat-parser": "^2.7.6",
19
18
  "@formatjs/intl": "^2.10.1",
20
19
  "intl-messageformat": "^10.5.11"
21
20
  },
package/src/format.ts ADDED
@@ -0,0 +1,224 @@
1
+ /**
2
+ * The core formatter, responsible for applying values to placeholders in
3
+ * messages and creating a static CST from the result. This method does _not_
4
+ * replace any of the rich-text formatting values (e.g., `$b` or `$link`) and
5
+ * leaves those to final formatters to apply as appropriate for the context
6
+ * of where the message is being rendered (e.g., React versus a plain string).
7
+ *
8
+ * The majority of this method is lifted from FormatJS' own `formatToParts`
9
+ * implementation, but is abstracted here to provide a few benefits:
10
+ * - working with the keyless AST format rather than having to transpose to the
11
+ * exact object format that FormatJS expects.
12
+ * - understanding the difference between rich text formatting values and
13
+ * user-provided values.
14
+ * - provide extra options related to the above for more control over how
15
+ * formatting operates, while abstracting out some of the common values.
16
+ * - adding extra context for hook and link functions to provide a `key`
17
+ * parameter, important to have React treat the resulting elements nicely.
18
+ */
19
+
20
+ import { FormatJsNode, FormatJsNodeType } from './keyless-json';
21
+ import { Formats, Formatters, MissingValueError } from 'intl-messageformat';
22
+ import { RichTextTagNames } from './types';
23
+
24
+ /**
25
+ * Returns true if the tag name should be considered a rich text tag that
26
+ * applies formatting to a message, rather than being a user-supplied value.
27
+ */
28
+ function isRichTextTag(name: string) {
29
+ return name[0] === '$';
30
+ }
31
+
32
+ export abstract class FormatBuilder<Result> {
33
+ abstract pushRichTextTag(tag: RichTextTagNames, children: Result[]): void;
34
+ abstract pushLiteralText(text: string): void;
35
+ abstract pushObject(value: object): void;
36
+ abstract finish(): Result[];
37
+ }
38
+
39
+ export type FormatBuilderConstructor<Result> = new () => FormatBuilder<Result>;
40
+
41
+ export function bindFormatValuesWithBuilder<T, Builder extends FormatBuilder<T>>(
42
+ builder: Builder,
43
+ nodes: FormatJsNode[],
44
+ locales: string | string[],
45
+ formatters: Formatters,
46
+ formats: Formats,
47
+ values: Record<string, string | object> = {},
48
+ currentPluralValue?: number,
49
+ originalMessage?: string,
50
+ ) {
51
+ // Hot path for static messages that are just parsed as a single string element.
52
+ if (nodes.length === 1 && nodes[0].type === FormatJsNodeType.Literal) {
53
+ builder.pushLiteralText(nodes[0].value);
54
+ return;
55
+ }
56
+
57
+ for (const node of nodes) {
58
+ switch (node.type) {
59
+ case FormatJsNodeType.Literal:
60
+ builder.pushLiteralText(node.value);
61
+ continue;
62
+
63
+ case FormatJsNodeType.Pound:
64
+ // Replace `#` in plural rules with the actual numeric value. Only
65
+ // numeric values are replaced, otherwise the value is completed ignored?
66
+ // Behavior copied from FormatJS directly.
67
+ if (typeof currentPluralValue === 'number') {
68
+ const value = formatters.getNumberFormat(locales).format(currentPluralValue);
69
+ builder.pushLiteralText(value);
70
+ }
71
+ continue;
72
+ }
73
+
74
+ const { value: variableName } = node;
75
+ // Enforce that all required values are provided by the caller, even if the
76
+ // actual value is falsy/undefined.
77
+ if (!(variableName in values) && !isRichTextTag(variableName)) {
78
+ throw new MissingValueError(variableName, originalMessage);
79
+ }
80
+
81
+ const value = values[variableName];
82
+
83
+ switch (node.type) {
84
+ case FormatJsNodeType.Argument:
85
+ // Empty values don't need to be added at all, they are purely for AST representation.
86
+ if (variableName == '$_') break;
87
+
88
+ if (typeof value === 'object') {
89
+ builder.pushObject(value);
90
+ } else {
91
+ // Taken from FormatJS: non-objects (strings, numbers, and falsy
92
+ // values) all get cast to strings immediately as literal nodes.
93
+ builder.pushLiteralText(String(value));
94
+ }
95
+ break;
96
+ case FormatJsNodeType.Date: {
97
+ // Distinct from FormatJS: We don't currently parse the skeleton ahead of time in the AST,
98
+ // 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();
103
+ // @ts-expect-error Cast string values to dates properly.
104
+ builder.pushLiteralText(formatters.getDateTimeFormat(locales, style).format(value));
105
+ break;
106
+ }
107
+ case FormatJsNodeType.Time: {
108
+ // Distinct from FormatJS: We don't currently parse the skeleton ahead of time in the AST,
109
+ // 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();
114
+ builder.pushLiteralText(
115
+ // @ts-expect-error Cast string values to dates properly.
116
+ formatters.getDateTimeFormat(locales, style).format(value),
117
+ );
118
+ break;
119
+ }
120
+ case FormatJsNodeType.Number: {
121
+ // Distinct from FormatJS: We don't currently parse the skeleton ahead of time in the AST,
122
+ // 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
+
128
+ // @ts-expect-error Support `scale` style property.
129
+ const scaledValue = value as number;
130
+ // const scaledValue = (value as number) * (style?.scale ?? 1);
131
+ builder.pushLiteralText(formatters.getNumberFormat(locales, style).format(scaledValue));
132
+ break;
133
+ }
134
+
135
+ case FormatJsNodeType.Tag: {
136
+ const { children } = node;
137
+ const appliedChildren = bindFormatValues(
138
+ builder.constructor as FormatBuilderConstructor<T>,
139
+ children,
140
+ locales,
141
+ formatters,
142
+ formats,
143
+ values,
144
+ currentPluralValue,
145
+ );
146
+ if (isRichTextTag(variableName)) {
147
+ builder.pushRichTextTag(variableName as RichTextTagNames, appliedChildren);
148
+ } else {
149
+ if (typeof value !== 'function') {
150
+ throw `expected a function type for a Tag formatting value, ${variableName}. got ${typeof value}: ${value}`;
151
+ }
152
+ let chunks = value(appliedChildren);
153
+ chunks = Array.isArray(chunks) ? chunks : [chunks];
154
+ for (const chunk of chunks) {
155
+ if (typeof chunk === 'string') {
156
+ builder.pushLiteralText(chunk);
157
+ } else {
158
+ builder.pushObject(chunk);
159
+ }
160
+ }
161
+ }
162
+ break;
163
+ }
164
+
165
+ case FormatJsNodeType.Select: {
166
+ const option = node.value in node.options ? node.options[node.value] : node.options.other;
167
+ if (option == null) {
168
+ throw `${node.value} is not a known option for select value ${variableName}. Valid options are ${Object.keys(node.options).join(', ')}`;
169
+ }
170
+ bindFormatValuesWithBuilder(builder, option.value, locales, formatters, formats, values);
171
+ break;
172
+ }
173
+
174
+ case FormatJsNodeType.Plural: {
175
+ const option = (() => {
176
+ const exactSelector = `=${value}`;
177
+ if (exactSelector in node.options) return node.options[exactSelector];
178
+ const rule = formatters
179
+ .getPluralRules(locales, { type: node.pluralType })
180
+ // @ts-expect-error Assert this `as number` properly.
181
+ .select((value as number) - (node.offset ?? 0));
182
+ return node.options[rule] ?? node.options.other;
183
+ })();
184
+
185
+ if (option == null) {
186
+ throw `${node.value} is not a known option for plural value ${variableName}. Valid options are ${Object.keys(node.options).join(', ')}`;
187
+ }
188
+ bindFormatValuesWithBuilder(
189
+ builder,
190
+ option.value,
191
+ locales,
192
+ formatters,
193
+ formats,
194
+ values,
195
+ // @ts-expect-error assert this `as number` properly.
196
+ (value as number) - (node.offset ?? 0),
197
+ );
198
+ break;
199
+ }
200
+ }
201
+ }
202
+ }
203
+
204
+ export function bindFormatValues<Result>(
205
+ Builder: FormatBuilderConstructor<Result>,
206
+ nodes: FormatJsNode[],
207
+ locales: string | string[],
208
+ formatters: Formatters,
209
+ formats: Formats,
210
+ values: Record<string, string | object> = {},
211
+ currentPluralValue?: number,
212
+ ): Result[] {
213
+ const builder = new Builder();
214
+ bindFormatValuesWithBuilder(
215
+ builder,
216
+ nodes,
217
+ locales,
218
+ formatters,
219
+ formats,
220
+ values,
221
+ currentPluralValue,
222
+ );
223
+ return builder.finish();
224
+ }