@discord/intl 0.26.2 → 0.26.4

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
@@ -20,12 +20,33 @@ import { AstNode } from '@discord/intl-ast';
20
20
  import type { RichTextTagNames } from './types';
21
21
  import { FormatConfigType } from './data-formatters/config';
22
22
  import { DataFormatters } from './data-formatters';
23
+ export interface BuilderContext {
24
+ keyPrefix: string;
25
+ }
23
26
  export declare abstract class FormatBuilder<Result, ObjectType = Result extends object ? Result : never> {
27
+ context: BuilderContext;
28
+ constructor(context: BuilderContext);
24
29
  abstract pushRichTextTag(tag: RichTextTagNames, children: Result[], control: Result[]): void;
25
30
  abstract pushLiteralText(text: string): void;
26
31
  abstract pushObject(value: ObjectType): void;
27
32
  abstract finish(): Result[];
28
33
  }
29
- export type FormatBuilderConstructor<Result> = new () => FormatBuilder<Result>;
30
- export declare function bindFormatValuesWithBuilder<T, ObjectType, Builder extends FormatBuilder<T, ObjectType>, FormatConfig extends FormatConfigType>(builder: Builder, nodes: AstNode[], locales: string | string[], formatters: DataFormatters<FormatConfig>, formatConfig: FormatConfig, values?: Record<string, string | object>, currentPluralValue?: number, originalMessage?: string): void;
31
- export declare function bindFormatValues<Result, FormatConfig extends FormatConfigType>(Builder: FormatBuilderConstructor<Result>, nodes: string | AstNode[], locales: string | string[], dataFormatters: DataFormatters<FormatConfig>, formatConfig: FormatConfig, values?: Record<string, string | object>, currentPluralValue?: number): Result[];
34
+ export type FormatBuilderConstructor<Result> = new (context: BuilderContext) => FormatBuilder<Result>;
35
+ export interface BindFormatValuesBase<FormatConfig extends FormatConfigType> {
36
+ originalMessage?: string;
37
+ nodes: string | AstNode[];
38
+ locales: string | string[];
39
+ dataFormatters: DataFormatters<FormatConfig>;
40
+ formatConfig: FormatConfig;
41
+ values: Record<string, string | object>;
42
+ currentPluralValue?: number;
43
+ keyPrefix: string;
44
+ }
45
+ export interface BindFormatValuesParams<Result, FormatConfig extends FormatConfigType> extends BindFormatValuesBase<FormatConfig> {
46
+ Builder: FormatBuilderConstructor<Result>;
47
+ }
48
+ export interface BindFormatValuesWithBuilderParams<T, ObjectType, Builder extends FormatBuilder<T, ObjectType>, FormatConfig extends FormatConfigType> extends BindFormatValuesBase<FormatConfig> {
49
+ builder: Builder;
50
+ }
51
+ export declare function bindFormatValuesWithBuilder<T, ObjectType, Builder extends FormatBuilder<T, ObjectType>, FormatConfig extends FormatConfigType>(params: BindFormatValuesWithBuilderParams<T, ObjectType, Builder, FormatConfig>): void;
52
+ export declare function bindFormatValues<Result, FormatConfig extends FormatConfigType>(params: BindFormatValuesParams<Result, FormatConfig>): Result[];
package/dist/format.js CHANGED
@@ -31,6 +31,9 @@ function isRichTextTag(name) {
31
31
  return name[0] === '$';
32
32
  }
33
33
  class FormatBuilder {
34
+ constructor(context) {
35
+ this.context = context;
36
+ }
34
37
  }
35
38
  exports.FormatBuilder = FormatBuilder;
36
39
  class MissingValueError extends Error {
@@ -41,15 +44,16 @@ class MissingValueError extends Error {
41
44
  this.nodeType = nodeType;
42
45
  }
43
46
  }
44
- function bindFormatValuesWithBuilder(builder, nodes, locales, formatters, formatConfig, values = {}, currentPluralValue, originalMessage) {
47
+ function bindFormatValuesWithBuilder(params) {
45
48
  var _a;
49
+ const { builder, originalMessage, nodes, locales, values, dataFormatters, formatConfig, currentPluralValue, keyPrefix, } = params;
46
50
  // Hot path for static messages that are just parsed as a single string element.
47
51
  if (nodes.length === 1 && typeof nodes[0] === 'string') {
48
52
  builder.pushLiteralText(nodes[0]);
49
53
  return;
50
54
  }
51
- let keyIndex = 0;
52
- for (const node of nodes) {
55
+ for (let keyIndex = 0; keyIndex < nodes.length; keyIndex++) {
56
+ const node = nodes[keyIndex];
53
57
  if (typeof node === 'string') {
54
58
  builder.pushLiteralText(node);
55
59
  continue;
@@ -61,7 +65,7 @@ function bindFormatValuesWithBuilder(builder, nodes, locales, formatters, format
61
65
  // numeric values are replaced, otherwise the value is completed ignored?
62
66
  // Behavior copied from FormatJS directly.
63
67
  if (typeof currentPluralValue === 'number') {
64
- const value = formatters.formatNumber(currentPluralValue);
68
+ const value = dataFormatters.formatNumber(currentPluralValue);
65
69
  builder.pushLiteralText(value);
66
70
  }
67
71
  continue;
@@ -100,7 +104,7 @@ function bindFormatValuesWithBuilder(builder, nodes, locales, formatters, format
100
104
  : nodeStyle != null
101
105
  ? (0, icu_skeleton_parser_1.parseDateTimeSkeleton)(nodeStyle)
102
106
  : undefined;
103
- builder.pushLiteralText(formatters.formatDate(value, style));
107
+ builder.pushLiteralText(dataFormatters.formatDate(value, style));
104
108
  break;
105
109
  }
106
110
  case intl_ast_1.FormatJsNodeType.Time: {
@@ -112,7 +116,7 @@ function bindFormatValuesWithBuilder(builder, nodes, locales, formatters, format
112
116
  : nodeStyle != null
113
117
  ? (0, icu_skeleton_parser_1.parseDateTimeSkeleton)(nodeStyle)
114
118
  : undefined;
115
- builder.pushLiteralText(formatters.formatTime(value, style));
119
+ builder.pushLiteralText(dataFormatters.formatTime(value, style));
116
120
  break;
117
121
  }
118
122
  case intl_ast_1.FormatJsNodeType.Number: {
@@ -127,15 +131,33 @@ function bindFormatValuesWithBuilder(builder, nodes, locales, formatters, format
127
131
  const scaledValue =
128
132
  // @ts-expect-error This is a weird cast that's not accurate, but works in the short term.
129
133
  typeof value !== 'number' ? value : value * ((_a = style === null || style === void 0 ? void 0 : style.scale) !== null && _a !== void 0 ? _a : 1);
130
- builder.pushLiteralText(formatters.formatNumber(scaledValue, style));
134
+ builder.pushLiteralText(dataFormatters.formatNumber(scaledValue, style));
131
135
  break;
132
136
  }
133
137
  case intl_ast_1.FormatJsNodeType.Tag: {
134
138
  const children = node[2 /* AstNodeIndices.Children */];
135
139
  const control = node[3 /* AstNodeIndices.Control */];
136
- const appliedChildren = bindFormatValues(builder.constructor, children, locales, formatters, formatConfig, values, currentPluralValue);
140
+ const appliedChildren = bindFormatValues({
141
+ Builder: builder.constructor,
142
+ nodes: children,
143
+ locales,
144
+ dataFormatters: dataFormatters,
145
+ formatConfig,
146
+ values,
147
+ currentPluralValue,
148
+ keyPrefix: `${keyPrefix}.${keyIndex}`,
149
+ });
137
150
  const appliedControl = control != null
138
- ? bindFormatValues(builder.constructor, control, locales, formatters, formatConfig, values, currentPluralValue)
151
+ ? bindFormatValues({
152
+ Builder: builder.constructor,
153
+ nodes: control,
154
+ locales,
155
+ dataFormatters: dataFormatters,
156
+ formatConfig,
157
+ values,
158
+ currentPluralValue,
159
+ keyPrefix: `${keyPrefix}.${keyIndex}-control`,
160
+ })
139
161
  : [];
140
162
  if (isRichTextTag(variableName)) {
141
163
  builder.pushRichTextTag(variableName, appliedChildren, appliedControl);
@@ -144,7 +166,7 @@ function bindFormatValuesWithBuilder(builder, nodes, locales, formatters, format
144
166
  if (typeof value !== 'function') {
145
167
  throw `expected a function type for a Tag formatting value, ${variableName}. got ${typeof value}: ${value}`;
146
168
  }
147
- let chunks = value(appliedChildren, `${keyIndex++}`);
169
+ let chunks = value(appliedChildren, `${keyPrefix}.${keyIndex}`);
148
170
  chunks = Array.isArray(chunks) ? chunks : [chunks];
149
171
  for (const chunk of chunks) {
150
172
  if (typeof chunk === 'string') {
@@ -164,7 +186,15 @@ function bindFormatValuesWithBuilder(builder, nodes, locales, formatters, format
164
186
  if (option == null) {
165
187
  throw `${requestedOption} is not a known option for select value ${variableName}. Valid options are ${Object.keys(options).join(', ')}`;
166
188
  }
167
- bindFormatValuesWithBuilder(builder, option, locales, formatters, formatConfig, values);
189
+ bindFormatValuesWithBuilder({
190
+ builder,
191
+ nodes: option,
192
+ locales,
193
+ dataFormatters,
194
+ formatConfig,
195
+ values,
196
+ keyPrefix: `${keyPrefix}.${keyIndex}`,
197
+ });
168
198
  break;
169
199
  }
170
200
  case intl_ast_1.FormatJsNodeType.Plural: {
@@ -176,7 +206,7 @@ function bindFormatValuesWithBuilder(builder, nodes, locales, formatters, format
176
206
  const exactSelector = `=${value}`;
177
207
  if (exactSelector in options)
178
208
  return options[exactSelector];
179
- const rule = formatters.getPluralRules({ type: pluralType }).select(
209
+ const rule = dataFormatters.getPluralRules({ type: pluralType }).select(
180
210
  // @ts-expect-error Assert this `as number` properly.
181
211
  value - (offset !== null && offset !== void 0 ? offset : 0));
182
212
  return (_a = options[rule]) !== null && _a !== void 0 ? _a : options.other;
@@ -184,22 +214,41 @@ function bindFormatValuesWithBuilder(builder, nodes, locales, formatters, format
184
214
  if (option == null) {
185
215
  throw `${value} is not a known option for plural value ${variableName}. Valid options are ${Object.keys(options).join(', ')}`;
186
216
  }
187
- bindFormatValuesWithBuilder(builder, option, locales, formatters, formatConfig, values,
188
- // @ts-expect-error assert this `as number` properly
189
- value - (offset !== null && offset !== void 0 ? offset : 0));
217
+ bindFormatValuesWithBuilder({
218
+ builder,
219
+ nodes: option,
220
+ locales,
221
+ dataFormatters,
222
+ formatConfig,
223
+ values,
224
+ // @ts-expect-error assert this `as number` properly
225
+ currentPluralValue: value - (offset !== null && offset !== void 0 ? offset : 0),
226
+ keyPrefix: `${keyPrefix}.${keyIndex}`,
227
+ });
190
228
  break;
191
229
  }
192
230
  }
193
231
  }
194
232
  }
195
- function bindFormatValues(Builder, nodes, locales, dataFormatters, formatConfig, values = {}, currentPluralValue) {
196
- const builder = new Builder();
233
+ function bindFormatValues(params) {
234
+ const { Builder, originalMessage, nodes, locales, dataFormatters, formatConfig, values, currentPluralValue, keyPrefix, } = params;
235
+ const builder = new Builder({ keyPrefix });
197
236
  if (typeof nodes === 'string') {
198
237
  builder.pushLiteralText(nodes);
199
238
  return builder.finish();
200
239
  }
201
240
  else {
202
- bindFormatValuesWithBuilder(builder, nodes, locales, dataFormatters, formatConfig, values, currentPluralValue);
241
+ bindFormatValuesWithBuilder({
242
+ builder,
243
+ originalMessage,
244
+ nodes,
245
+ locales,
246
+ dataFormatters,
247
+ formatConfig,
248
+ values,
249
+ currentPluralValue,
250
+ keyPrefix,
251
+ });
203
252
  return builder.finish();
204
253
  }
205
254
  }
@@ -6,7 +6,7 @@
6
6
  import type { AnyIntlMessage, FormatterImplementation, FunctionTypes, RichTextFormattingMap } from '../types';
7
7
  import * as React from 'react';
8
8
  import { IntlManager } from '../intl-manager';
9
- import { FormatBuilderConstructor } from '../format';
9
+ import { BuilderContext, FormatBuilder, FormatBuilderConstructor } from '../format';
10
10
  export type ReactHandlerEvent = React.MouseEvent | React.KeyboardEvent;
11
11
  export type ReactClickHandler = (e: ReactHandlerEvent) => void;
12
12
  export type ReactFunctionTypes = FunctionTypes<React.ReactNode, ReactClickHandler | {
@@ -18,6 +18,9 @@ export type ReactFunctionTypes = FunctionTypes<React.ReactNode, ReactClickHandle
18
18
  onContextMenu: ReactClickHandler;
19
19
  }>;
20
20
  export declare const DEFAULT_REACT_RICH_TEXT_ELEMENTS: RichTextFormattingMap<ReactFunctionTypes['hook']>;
21
+ export type ReactBuilder = {
22
+ new (context: BuilderContext): FormatBuilder<React.ReactNode>;
23
+ };
21
24
  /**
22
25
  * A type representing any message that has been formatted using one of the
23
26
  * default React formatting methods in the `@discord/intl` system. Use this
@@ -29,7 +29,7 @@ function createReactBuilder(richTextElements) {
29
29
  this.result = [];
30
30
  }
31
31
  pushRichTextTag(tag, children, control) {
32
- this.result.push(richTextElements[tag](children, `${this._nodeKey++}`, control));
32
+ this.result.push(richTextElements[tag](children, `${this.context.keyPrefix}.tag-${this._nodeKey++}`, control));
33
33
  }
34
34
  pushLiteralText(text) {
35
35
  const last = this.result[this.result.length - 1];
@@ -93,7 +93,15 @@ class IntlManager {
93
93
  return resolved.reserialize();
94
94
  }
95
95
  bindFormatValues(Builder, message, values) {
96
- return (0, format_1.bindFormatValues)(Builder, message.ast, [this.currentLocale, this.defaultLocale], this.data, this.formatConfig, values);
96
+ return (0, format_1.bindFormatValues)({
97
+ Builder,
98
+ nodes: message.ast,
99
+ locales: [this.currentLocale, this.defaultLocale],
100
+ dataFormatters: this.data,
101
+ formatConfig: this.formatConfig,
102
+ values,
103
+ keyPrefix: '',
104
+ });
97
105
  }
98
106
  }
99
107
  exports.IntlManager = IntlManager;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@discord/intl",
3
- "version": "0.26.2",
3
+ "version": "0.26.4",
4
4
  "license": "MIT",
5
5
  "description": "Client runtime for managing messages and translations in a React project.",
6
6
  "main": "./dist/index.js",
@@ -23,7 +23,7 @@
23
23
  "@formatjs/intl": "^2.10.1",
24
24
  "@intrnl/xxhash64": "^0.1.2",
25
25
  "intl-messageformat": "^10.5.11",
26
- "@discord/intl-ast": "0.26.2"
26
+ "@discord/intl-ast": "0.26.4"
27
27
  },
28
28
  "devDependencies": {
29
29
  "@formatjs/intl-durationformat": "^0.7.3",
package/src/format.ts CHANGED
@@ -20,7 +20,7 @@
20
20
  import {
21
21
  parseDateTimeSkeleton,
22
22
  parseNumberSkeleton,
23
- parseNumberSkeletonFromString,
23
+ parseNumberSkeletonFromString
24
24
  } from '@formatjs/icu-skeleton-parser';
25
25
 
26
26
  import { AstNode, AstNodeIndices, FormatJsNodeType } from '@discord/intl-ast';
@@ -36,13 +36,22 @@ function isRichTextTag(name: string) {
36
36
  return name[0] === '$';
37
37
  }
38
38
 
39
+ export interface BuilderContext {
40
+ keyPrefix: string;
41
+ }
42
+
39
43
  export abstract class FormatBuilder<Result, ObjectType = Result extends object ? Result : never> {
44
+ constructor(public context: BuilderContext) {}
40
45
  abstract pushRichTextTag(tag: RichTextTagNames, children: Result[], control: Result[]): void;
41
46
  abstract pushLiteralText(text: string): void;
42
47
  abstract pushObject(value: ObjectType): void;
43
48
  abstract finish(): Result[];
44
49
  }
45
50
 
51
+ export type FormatBuilderConstructor<Result> = new (
52
+ context: BuilderContext,
53
+ ) => FormatBuilder<Result>;
54
+
46
55
  class MissingValueError extends Error {
47
56
  constructor(
48
57
  public variableName: string,
@@ -55,31 +64,56 @@ class MissingValueError extends Error {
55
64
  }
56
65
  }
57
66
 
58
- export type FormatBuilderConstructor<Result> = new () => FormatBuilder<Result>;
67
+ export interface BindFormatValuesBase<FormatConfig extends FormatConfigType> {
68
+ originalMessage?: string;
69
+ nodes: string | AstNode[];
70
+ locales: string | string[];
71
+ dataFormatters: DataFormatters<FormatConfig>;
72
+ formatConfig: FormatConfig;
73
+ values: Record<string, string | object>;
74
+ currentPluralValue?: number;
75
+ keyPrefix: string;
76
+ }
77
+
78
+ export interface BindFormatValuesParams<Result, FormatConfig extends FormatConfigType>
79
+ extends BindFormatValuesBase<FormatConfig> {
80
+ Builder: FormatBuilderConstructor<Result>;
81
+ }
82
+
83
+ export interface BindFormatValuesWithBuilderParams<
84
+ T,
85
+ ObjectType,
86
+ Builder extends FormatBuilder<T, ObjectType>,
87
+ FormatConfig extends FormatConfigType,
88
+ > extends BindFormatValuesBase<FormatConfig> {
89
+ builder: Builder;
90
+ }
59
91
 
60
92
  export function bindFormatValuesWithBuilder<
61
93
  T,
62
94
  ObjectType,
63
95
  Builder extends FormatBuilder<T, ObjectType>,
64
96
  FormatConfig extends FormatConfigType,
65
- >(
66
- builder: Builder,
67
- nodes: AstNode[],
68
- locales: string | string[],
69
- formatters: DataFormatters<FormatConfig>,
70
- formatConfig: FormatConfig,
71
- values: Record<string, string | object> = {},
72
- currentPluralValue?: number,
73
- originalMessage?: string,
74
- ) {
97
+ >(params: BindFormatValuesWithBuilderParams<T, ObjectType, Builder, FormatConfig>) {
98
+ const {
99
+ builder,
100
+ originalMessage,
101
+ nodes,
102
+ locales,
103
+ values,
104
+ dataFormatters,
105
+ formatConfig,
106
+ currentPluralValue,
107
+ keyPrefix,
108
+ } = params;
75
109
  // Hot path for static messages that are just parsed as a single string element.
76
110
  if (nodes.length === 1 && typeof nodes[0] === 'string') {
77
111
  builder.pushLiteralText(nodes[0]);
78
112
  return;
79
113
  }
80
114
 
81
- let keyIndex = 0;
82
- for (const node of nodes) {
115
+ for (let keyIndex = 0; keyIndex < nodes.length; keyIndex++) {
116
+ const node = nodes[keyIndex];
83
117
  if (typeof node === 'string') {
84
118
  builder.pushLiteralText(node);
85
119
  continue;
@@ -91,7 +125,7 @@ export function bindFormatValuesWithBuilder<
91
125
  // numeric values are replaced, otherwise the value is completed ignored?
92
126
  // Behavior copied from FormatJS directly.
93
127
  if (typeof currentPluralValue === 'number') {
94
- const value = formatters.formatNumber(currentPluralValue);
128
+ const value = dataFormatters.formatNumber(currentPluralValue);
95
129
  builder.pushLiteralText(value);
96
130
  }
97
131
  continue;
@@ -132,7 +166,7 @@ export function bindFormatValuesWithBuilder<
132
166
  : nodeStyle != null
133
167
  ? parseDateTimeSkeleton(nodeStyle)
134
168
  : undefined;
135
- builder.pushLiteralText(formatters.formatDate(value as number | Date, style));
169
+ builder.pushLiteralText(dataFormatters.formatDate(value as number | Date, style));
136
170
  break;
137
171
  }
138
172
  case FormatJsNodeType.Time: {
@@ -145,7 +179,7 @@ export function bindFormatValuesWithBuilder<
145
179
  : nodeStyle != null
146
180
  ? parseDateTimeSkeleton(nodeStyle)
147
181
  : undefined;
148
- builder.pushLiteralText(formatters.formatTime(value as number | Date, style));
182
+ builder.pushLiteralText(dataFormatters.formatTime(value as number | Date, style));
149
183
  break;
150
184
  }
151
185
  case FormatJsNodeType.Number: {
@@ -163,33 +197,35 @@ export function bindFormatValuesWithBuilder<
163
197
  const scaledValue =
164
198
  // @ts-expect-error This is a weird cast that's not accurate, but works in the short term.
165
199
  typeof value !== 'number' ? (value as number) : (value as number) * (style?.scale ?? 1);
166
- builder.pushLiteralText(formatters.formatNumber(scaledValue, style));
200
+ builder.pushLiteralText(dataFormatters.formatNumber(scaledValue, style));
167
201
  break;
168
202
  }
169
203
 
170
204
  case FormatJsNodeType.Tag: {
171
205
  const children = node[AstNodeIndices.Children];
172
206
  const control = node[AstNodeIndices.Control];
173
- const appliedChildren = bindFormatValues(
174
- builder.constructor as FormatBuilderConstructor<T>,
175
- children,
207
+ const appliedChildren = bindFormatValues({
208
+ Builder: builder.constructor as FormatBuilderConstructor<T>,
209
+ nodes: children,
176
210
  locales,
177
- formatters,
211
+ dataFormatters: dataFormatters,
178
212
  formatConfig,
179
213
  values,
180
214
  currentPluralValue,
181
- );
215
+ keyPrefix: `${keyPrefix}.${keyIndex}`,
216
+ });
182
217
  const appliedControl =
183
218
  control != null
184
- ? bindFormatValues(
185
- builder.constructor as FormatBuilderConstructor<T>,
186
- control,
219
+ ? bindFormatValues({
220
+ Builder: builder.constructor as FormatBuilderConstructor<T>,
221
+ nodes: control,
187
222
  locales,
188
- formatters,
223
+ dataFormatters: dataFormatters,
189
224
  formatConfig,
190
225
  values,
191
226
  currentPluralValue,
192
- )
227
+ keyPrefix: `${keyPrefix}.${keyIndex}-control`,
228
+ })
193
229
  : [];
194
230
  if (isRichTextTag(variableName)) {
195
231
  builder.pushRichTextTag(
@@ -201,7 +237,7 @@ export function bindFormatValuesWithBuilder<
201
237
  if (typeof value !== 'function') {
202
238
  throw `expected a function type for a Tag formatting value, ${variableName}. got ${typeof value}: ${value}`;
203
239
  }
204
- let chunks = value(appliedChildren, `${keyIndex++}`);
240
+ let chunks = value(appliedChildren, `${keyPrefix}.${keyIndex}`);
205
241
  chunks = Array.isArray(chunks) ? chunks : [chunks];
206
242
  for (const chunk of chunks) {
207
243
  if (typeof chunk === 'string') {
@@ -221,7 +257,15 @@ export function bindFormatValuesWithBuilder<
221
257
  if (option == null) {
222
258
  throw `${requestedOption} is not a known option for select value ${variableName}. Valid options are ${Object.keys(options).join(', ')}`;
223
259
  }
224
- bindFormatValuesWithBuilder(builder, option, locales, formatters, formatConfig, values);
260
+ bindFormatValuesWithBuilder({
261
+ builder,
262
+ nodes: option,
263
+ locales,
264
+ dataFormatters,
265
+ formatConfig,
266
+ values,
267
+ keyPrefix: `${keyPrefix}.${keyIndex}`,
268
+ });
225
269
  break;
226
270
  }
227
271
 
@@ -233,7 +277,7 @@ export function bindFormatValuesWithBuilder<
233
277
  const exactSelector = `=${value}`;
234
278
  if (exactSelector in options) return options[exactSelector];
235
279
 
236
- const rule = formatters.getPluralRules({ type: pluralType }).select(
280
+ const rule = dataFormatters.getPluralRules({ type: pluralType }).select(
237
281
  // @ts-expect-error Assert this `as number` properly.
238
282
  (value as number) - (offset ?? 0),
239
283
  );
@@ -244,16 +288,18 @@ export function bindFormatValuesWithBuilder<
244
288
  throw `${value} is not a known option for plural value ${variableName}. Valid options are ${Object.keys(options).join(', ')}`;
245
289
  }
246
290
 
247
- bindFormatValuesWithBuilder(
291
+ bindFormatValuesWithBuilder({
248
292
  builder,
249
- option,
293
+ nodes: option,
250
294
  locales,
251
- formatters,
295
+ dataFormatters,
252
296
  formatConfig,
253
297
  values,
254
298
  // @ts-expect-error assert this `as number` properly
255
- (value as number) - (offset ?? 0),
256
- );
299
+ currentPluralValue: (value as number) - (offset ?? 0),
300
+ keyPrefix: `${keyPrefix}.${keyIndex}`,
301
+ });
302
+
257
303
  break;
258
304
  }
259
305
  }
@@ -261,28 +307,35 @@ export function bindFormatValuesWithBuilder<
261
307
  }
262
308
 
263
309
  export function bindFormatValues<Result, FormatConfig extends FormatConfigType>(
264
- Builder: FormatBuilderConstructor<Result>,
265
- nodes: string | AstNode[],
266
- locales: string | string[],
267
- dataFormatters: DataFormatters<FormatConfig>,
268
- formatConfig: FormatConfig,
269
- values: Record<string, string | object> = {},
270
- currentPluralValue?: number,
310
+ params: BindFormatValuesParams<Result, FormatConfig>,
271
311
  ): Result[] {
272
- const builder = new Builder();
312
+ const {
313
+ Builder,
314
+ originalMessage,
315
+ nodes,
316
+ locales,
317
+ dataFormatters,
318
+ formatConfig,
319
+ values,
320
+ currentPluralValue,
321
+ keyPrefix,
322
+ } = params;
323
+ const builder = new Builder({ keyPrefix });
273
324
  if (typeof nodes === 'string') {
274
325
  builder.pushLiteralText(nodes);
275
326
  return builder.finish();
276
327
  } else {
277
- bindFormatValuesWithBuilder(
328
+ bindFormatValuesWithBuilder({
278
329
  builder,
330
+ originalMessage,
279
331
  nodes,
280
332
  locales,
281
333
  dataFormatters,
282
334
  formatConfig,
283
335
  values,
284
336
  currentPluralValue,
285
- );
337
+ keyPrefix,
338
+ });
286
339
  return builder.finish();
287
340
  }
288
341
  }
@@ -8,11 +8,11 @@ import type {
8
8
  FormatterImplementation,
9
9
  FunctionTypes,
10
10
  RichTextFormattingMap,
11
- RichTextTagNames,
11
+ RichTextTagNames
12
12
  } from '../types';
13
13
  import * as React from 'react';
14
14
  import { IntlManager } from '../intl-manager';
15
- import { FormatBuilder, FormatBuilderConstructor } from '../format';
15
+ import { BuilderContext, FormatBuilder, FormatBuilderConstructor } from '../format';
16
16
 
17
17
  export type ReactHandlerEvent = React.MouseEvent | React.KeyboardEvent;
18
18
  export type ReactClickHandler = (e: ReactHandlerEvent) => void;
@@ -35,6 +35,10 @@ export const DEFAULT_REACT_RICH_TEXT_ELEMENTS: RichTextFormattingMap<ReactFuncti
35
35
  $p: (content, key) => h('p', { key }, content),
36
36
  };
37
37
 
38
+ export type ReactBuilder = {
39
+ new (context: BuilderContext): FormatBuilder<React.ReactNode>;
40
+ };
41
+
38
42
  /**
39
43
  * Creates a new `FormatBuilder` class that constructs a React element tree
40
44
  * from the message using the given `richTextElements` to apply formatting.
@@ -42,9 +46,9 @@ export const DEFAULT_REACT_RICH_TEXT_ELEMENTS: RichTextFormattingMap<ReactFuncti
42
46
  * overrides for rendering elements like links, paragraphs, and code blocks.
43
47
  * @param richTextElements
44
48
  */
45
- function createReactBuilder(richTextElements: RichTextFormattingMap<ReactFunctionTypes['hook']>): {
46
- new (): FormatBuilder<React.ReactNode>;
47
- } {
49
+ function createReactBuilder(
50
+ richTextElements: RichTextFormattingMap<ReactFunctionTypes['hook']>,
51
+ ): ReactBuilder {
48
52
  return class extends FormatBuilder<React.ReactNode> {
49
53
  _nodeKey: number = 0;
50
54
  result: React.ReactNode[] = [];
@@ -54,7 +58,13 @@ function createReactBuilder(richTextElements: RichTextFormattingMap<ReactFunctio
54
58
  children: React.ReactNode[],
55
59
  control: React.ReactNode[],
56
60
  ) {
57
- this.result.push(richTextElements[tag](children, `${this._nodeKey++}`, control));
61
+ this.result.push(
62
+ richTextElements[tag](
63
+ children,
64
+ `${this.context.keyPrefix}.tag-${this._nodeKey++}`,
65
+ control,
66
+ ),
67
+ );
58
68
  }
59
69
 
60
70
  pushLiteralText(text: string) {
@@ -1,11 +1,6 @@
1
1
  import { DEFAULT_FORMAT_CONFIG, type FormatConfigType } from './data-formatters/config';
2
2
  import { bindFormatValues, FormatBuilderConstructor } from './format';
3
- import type {
4
- FormatterImplementation,
5
- IntlMessageGetter,
6
- RequiredFormatValues,
7
- TypedIntlMessageGetter,
8
- } from './types';
3
+ import type { FormatterImplementation, IntlMessageGetter, RequiredFormatValues, TypedIntlMessageGetter } from './types';
9
4
  import { InternalIntlMessage } from './message';
10
5
  import { DataFormatters, makeDataFormatters } from './data-formatters';
11
6
 
@@ -193,13 +188,14 @@ export class IntlManager<
193
188
  message: InternalIntlMessage,
194
189
  values: Record<string, any>,
195
190
  ): T[] {
196
- return bindFormatValues(
191
+ return bindFormatValues({
197
192
  Builder,
198
- message.ast,
199
- [this.currentLocale, this.defaultLocale],
200
- this.data,
201
- this.formatConfig,
193
+ nodes: message.ast,
194
+ locales: [this.currentLocale, this.defaultLocale],
195
+ dataFormatters: this.data,
196
+ formatConfig: this.formatConfig,
202
197
  values,
203
- );
198
+ keyPrefix: '',
199
+ });
204
200
  }
205
201
  }