@khanacademy/perseus 21.6.0 → 22.0.1

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.
Files changed (62) hide show
  1. package/dist/article-renderer.d.ts +6 -0
  2. package/dist/components/highlighting/ui/highlight-set-renderer.d.ts +14 -0
  3. package/dist/components/highlighting/ui/highlighting-ui.d.ts +17 -0
  4. package/dist/components/i18n-context.d.ts +16 -0
  5. package/dist/components/input-with-examples.d.ts +6 -0
  6. package/dist/components/math-input.d.ts +22 -2
  7. package/dist/components/number-input.d.ts +6 -0
  8. package/dist/es/index.js +883 -496
  9. package/dist/es/index.js.map +1 -1
  10. package/dist/es/strings.js +312 -0
  11. package/dist/es/strings.js.map +1 -0
  12. package/dist/hint-renderer.d.ts +6 -0
  13. package/dist/hints-renderer.d.ts +6 -0
  14. package/dist/index.d.ts +1 -0
  15. package/dist/index.js +1099 -534
  16. package/dist/index.js.map +1 -1
  17. package/dist/multi-items/multi-renderer.d.ts +6 -0
  18. package/dist/perseus-types.d.ts +3 -2
  19. package/dist/renderer.d.ts +2 -0
  20. package/dist/server-item-renderer.d.ts +6 -0
  21. package/dist/strings.d.ts +190 -0
  22. package/dist/strings.js +395 -119
  23. package/dist/strings.js.map +1 -0
  24. package/dist/types.d.ts +2 -1
  25. package/dist/util/answer-types.d.ts +4 -12
  26. package/dist/util.d.ts +2 -1
  27. package/dist/widgets/categorizer.d.ts +8 -1
  28. package/dist/widgets/definition.d.ts +6 -0
  29. package/dist/widgets/deprecated-standin/index.d.ts +6 -0
  30. package/dist/widgets/explanation.d.ts +6 -0
  31. package/dist/widgets/expression.d.ts +12 -1
  32. package/dist/widgets/graded-group-answer-bar.d.ts +6 -0
  33. package/dist/widgets/graded-group.d.ts +6 -0
  34. package/dist/widgets/group.d.ts +6 -0
  35. package/dist/widgets/image.d.ts +6 -0
  36. package/dist/widgets/input-number.d.ts +8 -1
  37. package/dist/widgets/interactive-graph.d.ts +6 -0
  38. package/dist/widgets/interactive-graphs/graphs/components/arrowhead.d.ts +1 -0
  39. package/dist/widgets/interactive-graphs/interactive-graph-question-builder.d.ts +21 -0
  40. package/dist/widgets/interactive-graphs/locked-point.d.ts +1 -1
  41. package/dist/widgets/interactive-graphs/mafs-graph.d.ts +1 -0
  42. package/dist/widgets/interactive-graphs/reducer/use-graph-config.d.ts +1 -0
  43. package/dist/widgets/label-image/marker.d.ts +6 -0
  44. package/dist/widgets/label-image.d.ts +6 -0
  45. package/dist/widgets/matcher.d.ts +6 -0
  46. package/dist/widgets/matrix.d.ts +9 -1
  47. package/dist/widgets/molecule.d.ts +6 -0
  48. package/dist/widgets/number-line.d.ts +6 -0
  49. package/dist/widgets/numeric-input.d.ts +8 -1
  50. package/dist/widgets/passage-ref-target.d.ts +6 -0
  51. package/dist/widgets/passage-ref.d.ts +6 -0
  52. package/dist/widgets/passage.d.ts +6 -0
  53. package/dist/widgets/plotter.d.ts +6 -0
  54. package/dist/widgets/radio/choice-a11y-text.d.ts +2 -1
  55. package/dist/widgets/radio/radio.d.ts +8 -1
  56. package/dist/widgets/radio/util.d.ts +2 -1
  57. package/dist/widgets/table.d.ts +7 -0
  58. package/dist/widgets/unit.d.ts +8 -1
  59. package/dist/widgets/video.d.ts +6 -0
  60. package/dist/widgets.d.ts +2 -1
  61. package/package.json +18 -6
  62. package/dist/widgets/label-image/strings.d.ts +0 -3
package/dist/es/index.js CHANGED
@@ -1,13 +1,12 @@
1
1
  import * as KAS from '@khanacademy/kas';
2
- import { keyTranslator, CursorContext, createMathField, getCursorContext, DesktopKeypad, convertDotToTimesByLocale, mathQuillInstance, KeypadInput, KeypadType, KeyArray, keypadElementPropType } from '@khanacademy/math-input';
2
+ import { MathInputI18nContext, CursorContext, getKeyTranslator, createMathField, getCursorContext, DesktopKeypad, convertDotToTimesByLocale, mathQuillInstance, KeypadInput, KeypadType, KeyArray, keypadElementPropType } from '@khanacademy/math-input';
3
3
  import * as PerseusLinter from '@khanacademy/perseus-linter';
4
4
  import { linterContextDefault, Rule, linterContextProps } from '@khanacademy/perseus-linter';
5
5
  import { View, RenderStateRoot, useUniqueIdWithMock, UniqueIDProvider, Text as Text$2 } from '@khanacademy/wonder-blocks-core';
6
- import * as i18n from '@khanacademy/wonder-blocks-i18n';
7
6
  import Tooltip$1 from '@khanacademy/wonder-blocks-tooltip';
8
7
  import classNames from 'classnames';
9
8
  import * as React from 'react';
10
- import React__default, { useState, useEffect, useRef, createContext, useImperativeHandle } from 'react';
9
+ import React__default, { useContext, useState, useEffect, useRef, createContext, useImperativeHandle } from 'react';
11
10
  import _, { debounce as debounce$1 } from 'underscore';
12
11
  import Clickable from '@khanacademy/wonder-blocks-clickable';
13
12
  import { Popover, PopoverContentCore, PopoverContent } from '@khanacademy/wonder-blocks-popover';
@@ -69,6 +68,211 @@ function _objectWithoutPropertiesLoose(source, excluded) {
69
68
  return target;
70
69
  }
71
70
 
71
+ /**
72
+ * The translated strings that are used to render Perseus.
73
+ */
74
+
75
+ /**
76
+ * Mock strings for the Perseus package, to be used for tests and Storybook.
77
+ */
78
+ const mockStrings = {
79
+ closeKeypad: "close math keypad",
80
+ openKeypad: "open math keypad",
81
+ removeHighlight: "Remove highlight",
82
+ addHighlight: "Add highlight",
83
+ hintPos: ({
84
+ pos
85
+ }) => `Hint #${pos}`,
86
+ errorRendering: ({
87
+ error
88
+ }) => `Error rendering: ${error}`,
89
+ APPROXIMATED_PI_ERROR: "Your answer is close, but you may " + "have approximated pi. Enter your " + "answer as a multiple of pi, like " + "<code>12\\ \\text{pi}</code> or " + "<code>2/3\\ \\text{pi}</code>",
90
+ EXTRA_SYMBOLS_ERROR: "We could not understand your " + "answer. Please check your answer for extra " + "text or symbols.",
91
+ NEEDS_TO_BE_SIMPLFIED_ERROR: "Your answer is almost correct, " + "but it needs to be simplified.",
92
+ MISSING_PERCENT_ERROR: "Your answer is almost correct, " + "but it is missing a " + "<code>\\%</code> at the end.",
93
+ MULTIPLICATION_SIGN_ERROR: "I'm a computer. I only understand " + "multiplication if you use an asterisk " + "(*) as the multiplication sign.",
94
+ WRONG_CASE_ERROR: "Your answer includes use of a variable with the wrong case.",
95
+ WRONG_LETTER_ERROR: "Your answer includes a wrong variable letter.",
96
+ invalidSelection: "Make sure you select something for every row.",
97
+ ERROR_TITLE: "Oops!",
98
+ ERROR_MESSAGE: "Sorry, I don't understand that!",
99
+ hints: "Hints",
100
+ getAnotherHint: "Get another hint",
101
+ deprecatedStandin: "Sorry, this part of the question is no longer available. 😅 Don't worry, you won't be graded on this part. Keep going!",
102
+ keepTrying: "Keep trying",
103
+ tryAgain: "Try again",
104
+ check: "Check",
105
+ correctExcited: "Correct!",
106
+ nextQuestion: "Next question",
107
+ skipToTitle: ({
108
+ title
109
+ }) => `Skip to ${title}`,
110
+ current: "Current",
111
+ correct: "Correct",
112
+ correctSelected: "Correct (selected)",
113
+ correctCrossedOut: "Correct (but you crossed it out)",
114
+ incorrect: "Incorrect",
115
+ incorrectSelected: "Incorrect (selected)",
116
+ hideExplanation: "Hide explanation",
117
+ explain: "Explain",
118
+ INVALID_MESSAGE_PREFIX: "We couldn't grade your answer.",
119
+ DEFAULT_INVALID_MESSAGE_1: "It looks like you left something blank or ",
120
+ DEFAULT_INVALID_MESSAGE_2: "entered in an invalid answer.",
121
+ integerExample: "an integer, like $6$",
122
+ properExample: "a *proper* fraction, like $1/2$ or $6/10$",
123
+ simplifiedProperExample: "a *simplified proper* fraction, like $3/5$",
124
+ improperExample: "an *improper* fraction, like $10/7$ or $14/8$",
125
+ simplifiedImproperExample: "a *simplified improper* fraction, like $7/4$",
126
+ mixedExample: "a mixed number, like $1\\ 3/4$",
127
+ decimalExample: "an *exact* decimal, like $0.75$",
128
+ percentExample: "a percent, like $12.34\\%$",
129
+ piExample: "a multiple of pi, like $12\\ \\text{pi}$ or $2/3\\ \\text{pi}$",
130
+ yourAnswer: "**Your answer should be** ",
131
+ yourAnswerLabel: "Your answer:",
132
+ addPoints: "Click to add points",
133
+ addVertices: "Click to add vertices",
134
+ tapMultiple: "Tap each dot on the image to select all answers that apply.",
135
+ tapSingle: "Tap each dot on the image to select an answer.",
136
+ clickMultiple: "Click each dot on the image to select all answers that apply.",
137
+ clickSingle: "Click each dot on the image to select an answer.",
138
+ choices: "Choices:",
139
+ answers: ({
140
+ num
141
+ }) => num === 1 ? `${num} answer` : `${num} answers`,
142
+ hideAnswersToggleLabel: "Hide answer choices",
143
+ moves: ({
144
+ num
145
+ }) => num === 1 ? `Moves: ${num}` : `Moves: ${num}`,
146
+ clickTiles: "Click on the tiles to change the lights.",
147
+ turnOffLights: "You must turn on all of the lights to continue.",
148
+ fillAllCells: "Make sure you fill in all cells in the matrix.",
149
+ molecularDrawing: ({
150
+ content
151
+ }) => `A molecular structure drawing. SMILES notation: ${content}`,
152
+ switchDirection: "Switch direction",
153
+ circleOpen: "Make circle open",
154
+ circleFilled: "Make circle filled",
155
+ numDivisions: "Number of divisions:",
156
+ divisions: ({
157
+ divRangeString
158
+ }) => `Please make sure the number of divisions is in the range ${divRangeString}.`,
159
+ lineRange: ({
160
+ lineRange
161
+ }) => `lines ${lineRange}`,
162
+ lineNumber: ({
163
+ lineNumber
164
+ }) => `line ${lineNumber}`,
165
+ symbolPassage: ({
166
+ questionSymbol,
167
+ questionNumber
168
+ }) => `The symbol ${questionSymbol} indicates that question ${questionNumber} references this portion of the passage.`,
169
+ symbolQuestion: ({
170
+ sentenceSymbol
171
+ }) => ` The symbol ${sentenceSymbol} indicates that the following sentence is referenced in a question.`,
172
+ lineLabel: "Line",
173
+ beginningPassage: "Beginning of reading passage.",
174
+ beginningFootnotes: "Beginning of reading passage footnotes.",
175
+ endPassage: "End of reading passage.",
176
+ questionMarker: ({
177
+ number
178
+ }) => `[Marker for question ${number}]`,
179
+ circleMarker: ({
180
+ number
181
+ }) => `[Circle marker ${number}]`,
182
+ sentenceMarker: ({
183
+ number
184
+ }) => `[Sentence ${number}]`,
185
+ dragHandles: "Drag handles to make graph",
186
+ tapAddPoints: "Tap to add points",
187
+ false: "False",
188
+ true: "True",
189
+ no: "No",
190
+ yes: "Yes",
191
+ chooseCorrectNum: "Please choose the correct number of answers.",
192
+ notNoneOfTheAbove: "'None of the above' may not be selected when other answers are selected.",
193
+ noneOfTheAbove: "None of the above",
194
+ chooseNumAnswers: ({
195
+ numCorrect
196
+ }) => `Choose ${numCorrect} answers:`,
197
+ chooseAllAnswers: "Choose all answers that apply:",
198
+ chooseOneAnswer: "Choose 1 answer:",
199
+ choiceCheckedCorrect: ({
200
+ letter
201
+ }) => `(Choice ${letter}, Checked, Correct)`,
202
+ choiceCrossedOutCorrect: ({
203
+ letter
204
+ }) => `(Choice ${letter}, Crossed out, Correct)`,
205
+ choiceCorrect: ({
206
+ letter
207
+ }) => `(Choice ${letter}, Correct)`,
208
+ choiceCheckedIncorrect: ({
209
+ letter
210
+ }) => `(Choice ${letter}, Checked, Incorrect)`,
211
+ choiceCrossedOutIncorrect: ({
212
+ letter
213
+ }) => `(Choice ${letter}, Crossed out, Incorrect)`,
214
+ choiceIncorrect: ({
215
+ letter
216
+ }) => `(Choice ${letter}, Incorrect)`,
217
+ choiceChecked: ({
218
+ letter
219
+ }) => `(Choice ${letter}, Checked)`,
220
+ choiceCrossedOut: ({
221
+ letter
222
+ }) => `(Choice ${letter}, Crossed out)`,
223
+ choice: ({
224
+ letter
225
+ }) => `(Choice ${letter})`,
226
+ crossOut: "Cross out",
227
+ crossOutOption: "Cross out option",
228
+ crossOutChoice: ({
229
+ letter
230
+ }) => `Cross out Choice ${letter}`,
231
+ bringBack: "Bring back",
232
+ openMenuForChoice: ({
233
+ letter
234
+ }) => `Open menu for Choice ${letter}`,
235
+ letters: "A B C D E F G H I J K L M N O P Q R S T U V W X Y Z",
236
+ rightArrow: "Reaction arrow pointing to the right.",
237
+ dontUnderstandUnits: "I couldn't understand those units.",
238
+ checkSigFigs: "Check your significant figures.",
239
+ answerNumericallyIncorrect: "That answer is numerically incorrect.",
240
+ checkUnits: "Check your units.",
241
+ dontUnderstand: "I don't understand that",
242
+ loading: "Loading...",
243
+ videoTranscript: "See video transcript",
244
+ somethingWrong: "Something went wrong.",
245
+ videoWrapper: "Khan Academy video wrapper"
246
+ };
247
+
248
+ /**
249
+ * PerseusI18nContext provides a way to set the strings and locale that are
250
+ * used inside the Perseus package.
251
+ *
252
+ */
253
+ // @ts-expect-error - TS2322 - Type 'Context<{ strings: {}; locale: string; }>' is not assignable to type 'Context<I18nContextType>'.
254
+ const PerseusI18nContext = /*#__PURE__*/React.createContext(process.env.NODE_ENV === "test" || process.env.STORYBOOK ? {
255
+ strings: mockStrings,
256
+ locale: "en"
257
+ } :
258
+ // We want to return null here, not an empty object, so that we
259
+ // are will throw an error when attempting to access the
260
+ // undefined locale or strings, making it easier to debug.
261
+ null);
262
+ function PerseusI18nContextProvider({
263
+ children,
264
+ strings,
265
+ locale
266
+ }) {
267
+ return /*#__PURE__*/React.createElement(PerseusI18nContext.Provider, {
268
+ value: {
269
+ strings,
270
+ locale
271
+ }
272
+ }, children);
273
+ }
274
+ const usePerseusI18n = () => useContext(PerseusI18nContext);
275
+
72
276
  const debounce = (func, delay) => {
73
277
  let timer = null;
74
278
  return (...args) => {
@@ -81,21 +285,8 @@ const debounce = (func, delay) => {
81
285
  };
82
286
  };
83
287
 
84
- const customKeyTranslator = _extends({}, keyTranslator, {
85
- // If there's something in the input that can become part of a
86
- // fraction, typing "/" puts it in the numerator. If not, typing
87
- // "/" does nothing. In that case, enter a \frac.
88
- FRAC: mathQuill => {
89
- const contents = mathQuill.latex();
90
- mathQuill.typedText("/");
91
- if (mathQuill.latex() === contents) {
92
- mathQuill.cmd("\\frac");
93
- }
94
- }
95
- });
96
-
97
288
  // A WYSIWYG math input that calls `onChange(LaTeX-string)`
98
- class MathInput extends React.Component {
289
+ class InnerMathInput extends React.Component {
99
290
  constructor(...args) {
100
291
  super(...args);
101
292
  // @ts-expect-error - TS2564 - Property 'mouseDown' has no initializer and is not definitely assigned in the constructor.
@@ -109,6 +300,21 @@ class MathInput extends React.Component {
109
300
  };
110
301
  this.insert = value => {
111
302
  const input = this.mathField();
303
+ const {
304
+ locale
305
+ } = this.context;
306
+ const customKeyTranslator = _extends({}, getKeyTranslator(locale), {
307
+ // If there's something in the input that can become part of a
308
+ // fraction, typing "/" puts it in the numerator. If not, typing
309
+ // "/" does nothing. In that case, enter a \frac.
310
+ FRAC: mathQuill => {
311
+ const contents = mathQuill.latex();
312
+ mathQuill.typedText("/");
313
+ if (mathQuill.latex() === contents) {
314
+ mathQuill.cmd("\\frac");
315
+ }
316
+ }
317
+ });
112
318
  const inputModifier = customKeyTranslator[value];
113
319
  if (inputModifier) {
114
320
  inputModifier(input, value);
@@ -131,8 +337,11 @@ class MathInput extends React.Component {
131
337
  };
132
338
  this.mathField = () => {
133
339
  if (!this.__mathField && this.__mathFieldWrapperRef) {
340
+ const {
341
+ locale
342
+ } = this.context;
134
343
  // Initialize MathQuill.MathField instance
135
- this.__mathField = createMathField(this.__mathFieldWrapperRef, baseConfig => _extends({}, baseConfig, {
344
+ this.__mathField = createMathField(this.__mathFieldWrapperRef, this.props.mathInputStrings, baseConfig => _extends({}, baseConfig, {
136
345
  handlers: {
137
346
  edit: debounce(mathField => {
138
347
  // This handler is guaranteed to be called on change, but
@@ -148,7 +357,7 @@ class MathInput extends React.Component {
148
357
  // Use the specified symbol to represent multiplication
149
358
  // TODO(alex): Add an option to disallow variables, in
150
359
  // which case 'x' should get converted to '\\times'
151
- if (convertDotToTimesByLocale(this.props.convertDotToTimes)) {
360
+ if (convertDotToTimesByLocale(locale, this.props.convertDotToTimes)) {
152
361
  value = value.replace(/\\cdot/g, "\\times");
153
362
 
154
363
  // Preserve cursor position in the common case:
@@ -208,7 +417,10 @@ class MathInput extends React.Component {
208
417
  focused: false
209
418
  });
210
419
  this.handleKeypadPress = (key, e) => {
211
- const translator = keyTranslator[key];
420
+ const {
421
+ locale
422
+ } = this.context;
423
+ const translator = getKeyTranslator(locale)[key];
212
424
  const mathField = this.mathField();
213
425
  if (mathField) {
214
426
  if (translator) {
@@ -307,7 +519,7 @@ class MathInput extends React.Component {
307
519
  focused: false,
308
520
  active: false
309
521
  }) : /*#__PURE__*/React.createElement(Clickable, {
310
- "aria-label": this.state.keypadOpen ? i18n._("close math keypad") : i18n._("open math keypad"),
522
+ "aria-label": this.state.keypadOpen ? this.context.strings.closeKeypad : this.context.strings.openKeypad,
311
523
  "aria-checked": this.state.keypadOpen,
312
524
  role: "switch",
313
525
  onClick: () => this.state.keypadOpen ? this.closeKeypad() : this.openKeypad()
@@ -316,10 +528,36 @@ class MathInput extends React.Component {
316
528
  }, props))))));
317
529
  }
318
530
  }
319
- MathInput.defaultProps = {
531
+
532
+ // We need to have two contexts (one for Perseus and one for MathInput), so we
533
+ // add a wrapper around the MathInput component to provide the MathInput context
534
+ // to it.
535
+ InnerMathInput.contextType = PerseusI18nContext;
536
+ InnerMathInput.defaultProps = {
320
537
  value: "",
321
538
  convertDotToTimes: false
322
539
  };
540
+ class MathInput extends React.Component {
541
+ constructor(...args) {
542
+ super(...args);
543
+ this.inputRef = /*#__PURE__*/React.createRef();
544
+ }
545
+ focus() {
546
+ var _this$inputRef$curren;
547
+ (_this$inputRef$curren = this.inputRef.current) == null || _this$inputRef$curren.focus();
548
+ }
549
+ insert(value) {
550
+ var _this$inputRef$curren2;
551
+ (_this$inputRef$curren2 = this.inputRef.current) == null || _this$inputRef$curren2.insert(value);
552
+ }
553
+ render() {
554
+ return /*#__PURE__*/React.createElement(InnerMathInput, _extends({}, this.props, {
555
+ ref: this.inputRef,
556
+ mathInputStrings: this.context.strings
557
+ }));
558
+ }
559
+ }
560
+ MathInput.contextType = MathInputI18nContext;
323
561
  const MathInputIcon = ({
324
562
  hovered,
325
563
  focused,
@@ -1235,16 +1473,6 @@ const MAXERROR_EPSILON = Math.pow(2, -42);
1235
1473
  // TOOD(kevinb): Figure out how this relates to KEScore in
1236
1474
  // perseus-all-package/types.js and see if there's a way to
1237
1475
  // unify these types.
1238
- const errors = {
1239
- APPROXIMATED_PI_ERROR: i18n._("Your answer is close, but you may " + "have approximated pi. Enter your " + "answer as a multiple of pi, like " + "<code>12\\ \\text{pi}</code> or " + "<code>2/3\\ \\text{pi}</code>"),
1240
- EXTRA_SYMBOLS_ERROR: i18n._("We could not understand your " + "answer. Please check your answer for extra " + "text or symbols."),
1241
- NEEDS_TO_BE_SIMPLFIED_ERROR: i18n._("Your answer is almost correct, " + "but it needs to be simplified."),
1242
- MISSING_PERCENT_ERROR: i18n._("Your answer is almost correct, " + "but it is missing a " + "<code>\\%</code> at the end."),
1243
- MULTIPLICATION_SIGN_ERROR: i18n._("I'm a computer. I only understand " + "multiplication if you use an asterisk " + "(*) as the multiplication sign."),
1244
- WRONG_CASE_ERROR: i18n._("Your answer includes use of a variable with the wrong case."),
1245
- WRONG_LETTER_ERROR: i18n._("Your answer includes a wrong variable letter.")
1246
- };
1247
-
1248
1476
  /*
1249
1477
  * Answer types
1250
1478
  *
@@ -1317,7 +1545,7 @@ const KhanAnswerTypes = {
1317
1545
  */
1318
1546
  predicate: {
1319
1547
  defaultForms: "integer, proper, improper, mixed, decimal",
1320
- createValidatorFunctional: function (predicate, options) {
1548
+ createValidatorFunctional: function (predicate, options, strings) {
1321
1549
  // Extract the options from the given solution object
1322
1550
  options = _.extend({
1323
1551
  simplify: "required",
@@ -1705,12 +1933,12 @@ const KhanAnswerTypes = {
1705
1933
  } else if (form === "percent") {
1706
1934
  // Otherwise, an error was returned
1707
1935
  score.empty = true;
1708
- score.message = errors.MISSING_PERCENT_ERROR;
1936
+ score.message = strings.MISSING_PERCENT_ERROR;
1709
1937
  } else {
1710
1938
  if (options.simplify !== "enforced") {
1711
1939
  score.empty = true;
1712
1940
  }
1713
- score.message = errors.NEEDS_TO_BE_SIMPLFIED_ERROR;
1941
+ score.message = strings.NEEDS_TO_BE_SIMPLFIED_ERROR;
1714
1942
  }
1715
1943
  // The return false below stops the looping of the
1716
1944
  // callback since predicate check succeeded.
@@ -1719,7 +1947,7 @@ const KhanAnswerTypes = {
1719
1947
  }
1720
1948
  if (piApprox && predicate(val, Math.abs(val * 0.001))) {
1721
1949
  score.empty = true;
1722
- score.message = errors.APPROXIMATED_PI_ERROR;
1950
+ score.message = strings.APPROXIMATED_PI_ERROR;
1723
1951
  }
1724
1952
  }
1725
1953
  });
@@ -1735,7 +1963,7 @@ const KhanAnswerTypes = {
1735
1963
  });
1736
1964
  if (!interpretedGuess) {
1737
1965
  score.empty = true;
1738
- score.message = errors.EXTRA_SYMBOLS_ERROR;
1966
+ score.message = strings.EXTRA_SYMBOLS_ERROR;
1739
1967
  return score;
1740
1968
  }
1741
1969
  }
@@ -1758,10 +1986,10 @@ const KhanAnswerTypes = {
1758
1986
  type: "predicate"
1759
1987
  })];
1760
1988
  },
1761
- createValidatorFunctional: function (correctAnswer, options) {
1989
+ createValidatorFunctional: function (correctAnswer, options, strings) {
1762
1990
  return KhanAnswerTypes.predicate.createValidatorFunctional(
1763
1991
  // @ts-expect-error - TS2556 - A spread argument must either have a tuple type or be passed to a rest parameter.
1764
- ...KhanAnswerTypes.number.convertToPredicate(correctAnswer, options));
1992
+ ...KhanAnswerTypes.number.convertToPredicate(correctAnswer, options), strings);
1765
1993
  }
1766
1994
  },
1767
1995
  /*
@@ -1826,7 +2054,7 @@ const KhanAnswerTypes = {
1826
2054
  }
1827
2055
  return solution;
1828
2056
  },
1829
- createValidatorFunctional: function (solution, options) {
2057
+ createValidatorFunctional: function (solution, options, strings) {
1830
2058
  return function (guess) {
1831
2059
  const score = {
1832
2060
  empty: false,
@@ -1877,7 +2105,7 @@ const KhanAnswerTypes = {
1877
2105
  // @ts-expect-error - TS2540 - Cannot assign to 'ungraded' because it is a read-only property.
1878
2106
  score.ungraded = true;
1879
2107
  // @ts-expect-error - TS2540 - Cannot assign to 'message' because it is a read-only property.
1880
- score.message = result.wrongVariableCase ? errors.WRONG_CASE_ERROR : errors.WRONG_LETTER_ERROR;
2108
+ score.message = result.wrongVariableCase ? strings.WRONG_CASE_ERROR : strings.WRONG_LETTER_ERROR;
1881
2109
  // Don't tell the use they're "almost there" in this case, that may not be true and isn't helpful.
1882
2110
  // @ts-expect-error - TS2339 - Property 'suppressAlmostThere' does not exist on type '{ readonly empty: false; readonly correct: false; readonly message: string | null | undefined; readonly guess: any; readonly ungraded: false; }'.
1883
2111
  score.suppressAlmostThere = true;
@@ -1903,7 +2131,7 @@ const KhanAnswerTypes = {
1903
2131
  // @ts-expect-error - TS2540 - Cannot assign to 'ungraded' because it is a read-only property.
1904
2132
  score.ungraded = true;
1905
2133
  // @ts-expect-error - TS2540 - Cannot assign to 'message' because it is a read-only property.
1906
- score.message = errors.MULTIPLICATION_SIGN_ERROR;
2134
+ score.message = strings.MULTIPLICATION_SIGN_ERROR;
1907
2135
  } else if (resultX.message) {
1908
2136
  // TODO(aasmund): I18nize `score.message`
1909
2137
  // @ts-expect-error - TS2540 - Cannot assign to 'message' because it is a read-only property.
@@ -1918,9 +2146,6 @@ const KhanAnswerTypes = {
1918
2146
  };
1919
2147
 
1920
2148
  const _excluded$5 = ["scoring"];
1921
- const ERROR_TITLE = i18n._("Oops!");
1922
- const ERROR_MESSAGE = i18n._("Sorry, I don't understand that!");
1923
-
1924
2149
  // Map of international operator names to their English equivalents
1925
2150
  const englishOperators = {
1926
2151
  arctg: "arctan",
@@ -1950,6 +2175,32 @@ const deriveKeypadVersion = apiOptions => {
1950
2175
  // can remove this `virtualKeypadVersion` field entirely.
1951
2176
  return apiOptions.nativeKeypadProxy != null ? "REACT_NATIVE_KEYPAD" : "MATH_INPUT_KEYPAD_V2";
1952
2177
  };
2178
+ /**
2179
+ * Get the character used for separating decimals.
2180
+ */
2181
+ const getDecimalSeparator = locale => {
2182
+ var _match$;
2183
+ switch (locale) {
2184
+ // TODO(somewhatabstract): Remove this when Chrome supports the `ka`
2185
+ // locale properly.
2186
+ // https://github.com/formatjs/formatjs/issues/1526#issuecomment-559891201
2187
+ //
2188
+ // Supported locales in Chrome:
2189
+ // https://source.chromium.org/chromium/chromium/src/+/master:third_party/icu/scripts/chrome_ui_languages.list
2190
+ case "ka":
2191
+ return ",";
2192
+ default:
2193
+ const numberWithDecimalSeparator = 1.1;
2194
+ // TODO(FEI-3647): Update to use .formatToParts() once we no longer have to
2195
+ // support Safari 12.
2196
+ const match = new Intl.NumberFormat(locale).format(numberWithDecimalSeparator)
2197
+ // 0x661 is ARABIC-INDIC DIGIT ONE
2198
+ // 0x6F1 is EXTENDED ARABIC-INDIC DIGIT ONE
2199
+ .match(/[^\d\u0661\u06F1]/);
2200
+ return (_match$ = match == null ? void 0 : match[0]) != null ? _match$ : ".";
2201
+ }
2202
+ };
2203
+
1953
2204
  // The new, MathQuill input expression widget
1954
2205
  class Expression extends React.Component {
1955
2206
  constructor(..._args) {
@@ -1982,7 +2233,7 @@ class Expression extends React.Component {
1982
2233
  if (!this.parse(this.props.value, this.props).parsed) {
1983
2234
  const apiResult = this.props.apiOptions.onInputError(null,
1984
2235
  // reserved for some widget identifier
1985
- this.props.value, ERROR_MESSAGE);
2236
+ this.props.value, this.context.strings.ERROR_TITLE);
1986
2237
  if (apiResult !== false) {
1987
2238
  this.setState({
1988
2239
  invalid: true
@@ -2006,7 +2257,7 @@ class Expression extends React.Component {
2006
2257
  scoring
2007
2258
  } = _ref,
2008
2259
  rubric = _objectWithoutPropertiesLoose(_ref, _excluded$5);
2009
- const score = Expression.validate(this.getUserInput(), rubric, onInputError || function () {});
2260
+ const score = Expression.validate(this.getUserInput(), rubric, onInputError || function () {}, this.context.strings, this.context.locale);
2010
2261
 
2011
2262
  // "scoring" is a flag that indicates when we are checking answers.
2012
2263
  // otherwise, we may just be checking validity after changes.
@@ -2033,7 +2284,7 @@ class Expression extends React.Component {
2033
2284
  // make it so that solution answers with ','s or '.'s work
2034
2285
  const options = _.pick(props || this.props, "functions");
2035
2286
  _.extend(options, {
2036
- decimal_separator: i18n.getDecimalSeparator()
2287
+ decimal_separator: getDecimalSeparator(this.context.locale)
2037
2288
  });
2038
2289
  return KAS.parse(normalizeTex(value), options);
2039
2290
  };
@@ -2069,9 +2320,10 @@ class Expression extends React.Component {
2069
2320
  this.refs.input.focus();
2070
2321
  };
2071
2322
  this.blurInputPath = inputPath => {
2323
+ var _this$refs$input;
2072
2324
  // eslint-disable-next-line react/no-string-refs
2073
2325
  // @ts-expect-error - TS2339 - Property 'blur' does not exist on type 'ReactInstance'.
2074
- this.refs.input.blur();
2326
+ (_this$refs$input = this.refs.input) == null || _this$refs$input.blur();
2075
2327
  };
2076
2328
  this.getInputPaths = () => {
2077
2329
  // The widget itself is an input, so we return a single empty list to
@@ -2111,10 +2363,10 @@ class Expression extends React.Component {
2111
2363
  */
2112
2364
  static validate(userInput, rubric,
2113
2365
  // @ts-expect-error - TS2322 - Type '() => void' is not assignable to type 'OnInputErrorFunctionType'.
2114
- onInputError = function () {}) {
2366
+ onInputError = function () {}, strings, locale) {
2115
2367
  const options = _.clone(rubric);
2116
2368
  _.extend(options, {
2117
- decimal_separator: i18n.getDecimalSeparator()
2369
+ decimal_separator: getDecimalSeparator(locale)
2118
2370
  });
2119
2371
  const createValidator = answer => {
2120
2372
  // We give options to KAS.parse here because it is parsing the
@@ -2137,7 +2389,7 @@ class Expression extends React.Component {
2137
2389
  return KhanAnswerTypes.expression.createValidatorFunctional(expression.expr, _({}).extend(options, {
2138
2390
  simplify: answer.simplify,
2139
2391
  form: answer.form
2140
- }));
2392
+ }), strings);
2141
2393
  };
2142
2394
 
2143
2395
  // Find the first answer form that matches the user's input and that
@@ -2280,6 +2532,10 @@ class Expression extends React.Component {
2280
2532
  "perseus-widget-expression": true,
2281
2533
  "show-error-tooltip": this.state.showErrorTooltip
2282
2534
  });
2535
+ const {
2536
+ ERROR_MESSAGE,
2537
+ ERROR_TITLE
2538
+ } = this.context.strings;
2283
2539
  return /*#__PURE__*/React.createElement("div", {
2284
2540
  className: className,
2285
2541
  onBlur: () => this.state.invalid && this.setState({
@@ -2328,6 +2584,7 @@ class Expression extends React.Component {
2328
2584
  * to be included as keys on the keypad. These are scraped from the answer
2329
2585
  * forms.
2330
2586
  */
2587
+ Expression.contextType = PerseusI18nContext;
2331
2588
  Expression.defaultProps = {
2332
2589
  value: "",
2333
2590
  times: false,
@@ -2715,7 +2972,7 @@ function keScoreFromPerseusScore(score, guess, state) {
2715
2972
  * Return the first valid interpretation of 'text' as a number, in the form
2716
2973
  * {value: 2.3, exact: true}.
2717
2974
  */
2718
- function firstNumericalParse$1(text) {
2975
+ function firstNumericalParse$1(text, strings) {
2719
2976
  // TODO(alpert): This is sort of hacky...
2720
2977
  let first;
2721
2978
  const val = KhanAnswerTypes.predicate.createValidatorFunctional(function (ans) {
@@ -2725,7 +2982,7 @@ function firstNumericalParse$1(text) {
2725
2982
  simplify: "optional",
2726
2983
  inexact: true,
2727
2984
  forms: "integer, proper, improper, pi, log, mixed, decimal"
2728
- });
2985
+ }, strings);
2729
2986
  val(text);
2730
2987
  return first;
2731
2988
  }
@@ -10277,8 +10534,6 @@ const getClipPoint = function getClipPoint(graph, coord, angle) {
10277
10534
  class WrappedDrawing {
10278
10535
  constructor(graphie, initialPoint) {
10279
10536
  this.graphie = void 0;
10280
- this.wrapper = void 0;
10281
- this.visibleShape = void 0;
10282
10537
  this.initialPoint = void 0;
10283
10538
  this.graphie = graphie;
10284
10539
  this.initialPoint = initialPoint;
@@ -18312,7 +18567,7 @@ const rules$1 = _extends({}, pureMarkdownRules, {
18312
18567
  // just a stub for testing.
18313
18568
  return /*#__PURE__*/React.createElement("em", {
18314
18569
  key: state.key
18315
- }, i18n.doNotTranslate("[Widget: ", node.id, "]"));
18570
+ }, `[Widget: ${node.id}]`);
18316
18571
  }
18317
18572
  }),
18318
18573
  blockMath: _extends({}, pureMarkdownRules.blockMath, {
@@ -18785,6 +19040,7 @@ const replaceDeprecatedEditors = () => {
18785
19040
  replaceEditor("simulator", "deprecated-standin");
18786
19041
  };
18787
19042
  const getWidget = name => {
19043
+ var _widgets$name;
18788
19044
  // TODO(alex): Consider referring to these as renderers to avoid
18789
19045
  // overloading "widget"
18790
19046
  if (!_.has(widgets, name)) {
@@ -18792,9 +19048,9 @@ const getWidget = name => {
18792
19048
  }
18793
19049
 
18794
19050
  // Allow widgets to specify a widget directly or via a function
18795
- if (widgets[name].getWidget) {
18796
- // @ts-expect-error - TS2722 - Cannot invoke an object which is possibly 'undefined'.
18797
- return widgets[name].getWidget();
19051
+ if ((_widgets$name = widgets[name]) != null && _widgets$name.getWidget) {
19052
+ var _widgets$name$getWidg, _widgets$name2;
19053
+ return (_widgets$name$getWidg = (_widgets$name2 = widgets[name]).getWidget) == null ? void 0 : _widgets$name$getWidg.call(_widgets$name2);
18798
19054
  }
18799
19055
  return widgets[name].widget;
18800
19056
  };
@@ -18943,7 +19199,7 @@ const upgradeWidgetInfoToLatestVersion = oldWidgetInfo => {
18943
19199
  options: newEditorProps
18944
19200
  });
18945
19201
  };
18946
- const getRendererPropsForWidgetInfo = (widgetInfo, problemNum) => {
19202
+ const getRendererPropsForWidgetInfo = (widgetInfo, strings, problemNum) => {
18947
19203
  const type = widgetInfo.type;
18948
19204
  const widgetExports = widgets[type];
18949
19205
  if (widgetExports == null) {
@@ -18964,7 +19220,7 @@ const getRendererPropsForWidgetInfo = (widgetInfo, problemNum) => {
18964
19220
  transform = widgetExports.transform || _.identity;
18965
19221
  }
18966
19222
  // widgetInfo.options are the widgetEditor's props:
18967
- return transform(widgetInfo.options, problemNum);
19223
+ return transform(widgetInfo.options, strings, problemNum);
18968
19224
  };
18969
19225
  const traverseChildWidgets$3 = (widgetInfo, traverseRenderer) => {
18970
19226
  if (!traverseRenderer) {
@@ -19363,8 +19619,11 @@ class Renderer extends React.Component {
19363
19619
  problemNum
19364
19620
  } = props;
19365
19621
  const widgetsStartProps = {};
19622
+ const {
19623
+ strings
19624
+ } = this.props;
19366
19625
  entries(allWidgetInfo).forEach(([key, widgetInfo]) => {
19367
- widgetsStartProps[key] = getRendererPropsForWidgetInfo(widgetInfo, problemNum);
19626
+ widgetsStartProps[key] = getRendererPropsForWidgetInfo(widgetInfo, strings, problemNum);
19368
19627
  });
19369
19628
 
19370
19629
  // Call the onWidgetStartProps callback if it exists
@@ -21424,10 +21683,12 @@ class InputWithExamples extends React.Component {
21424
21683
  id: this._getUniqueId()
21425
21684
  }, /*#__PURE__*/React.createElement(Renderer, {
21426
21685
  content: examplesContent,
21427
- linterContext: PerseusLinter.pushContextStack(this.props.linterContext, "input-with-examples")
21686
+ linterContext: PerseusLinter.pushContextStack(this.props.linterContext, "input-with-examples"),
21687
+ strings: this.context.strings
21428
21688
  })));
21429
21689
  }
21430
21690
  }
21691
+ InputWithExamples.contextType = PerseusI18nContext;
21431
21692
  InputWithExamples.defaultProps = {
21432
21693
  type: "text",
21433
21694
  shouldShowExamples: true,
@@ -21458,8 +21719,9 @@ class SimpleKeypadInput extends React.Component {
21458
21719
  }
21459
21720
 
21460
21721
  blur() {
21722
+ var _this$refs$input;
21461
21723
  // @ts-expect-error - TS2339 - Property 'blur' does not exist on type 'ReactInstance'.
21462
- this.refs.input.blur(); // eslint-disable-line react/no-string-refs
21724
+ (_this$refs$input = this.refs.input) == null || _this$refs$input.blur(); // eslint-disable-line react/no-string-refs
21463
21725
  }
21464
21726
 
21465
21727
  getValue() {
@@ -21549,32 +21811,32 @@ const answerTypes = {
21549
21811
  }
21550
21812
  };
21551
21813
  const formExamples$1 = {
21552
- integer: function (options) {
21553
- return i18n._("an integer, like $6$");
21814
+ integer: function (options, strings) {
21815
+ return strings.integerExample;
21554
21816
  },
21555
- proper: function (options) {
21817
+ proper: function (options, strings) {
21556
21818
  if (options.simplify === "optional") {
21557
- return i18n._("a *proper* fraction, like $1/2$ or $6/10$");
21819
+ return strings.properExample;
21558
21820
  }
21559
- return i18n._("a *simplified proper* fraction, like $3/5$");
21821
+ return strings.simplifiedProperExample;
21560
21822
  },
21561
- improper: function (options) {
21823
+ improper: function (options, strings) {
21562
21824
  if (options.simplify === "optional") {
21563
- return i18n._("an *improper* fraction, like $10/7$ or $14/8$");
21825
+ return strings.improperExample;
21564
21826
  }
21565
- return i18n._("a *simplified improper* fraction, like $7/4$");
21827
+ return strings.simplifiedImproperExample;
21566
21828
  },
21567
- mixed: function (options) {
21568
- return i18n._("a mixed number, like $1\\ 3/4$");
21829
+ mixed: function (options, strings) {
21830
+ return strings.mixedExample;
21569
21831
  },
21570
- decimal: function (options) {
21571
- return i18n._("an *exact* decimal, like $0.75$");
21832
+ decimal: function (options, strings) {
21833
+ return strings.decimalExample;
21572
21834
  },
21573
- percent: function (options) {
21574
- return i18n._("a percent, like $12.34\\%$");
21835
+ percent: function (options, strings) {
21836
+ return strings.percentExample;
21575
21837
  },
21576
- pi: function (options) {
21577
- return i18n._("a multiple of pi, like $12\\ \\text{pi}$ or " + "$2/3\\ \\text{pi}$");
21838
+ pi: function (options, strings) {
21839
+ return strings.piExample;
21578
21840
  }
21579
21841
  };
21580
21842
  class InputNumber extends React.Component {
@@ -21606,9 +21868,10 @@ class InputNumber extends React.Component {
21606
21868
  this.refs.input.focus();
21607
21869
  };
21608
21870
  this.blurInputPath = inputPath => {
21871
+ var _this$refs$input;
21609
21872
  // eslint-disable-next-line react/no-string-refs
21610
21873
  // @ts-expect-error - TS2339 - Property 'blur' does not exist on type 'ReactInstance'.
21611
- this.refs.input.blur();
21874
+ (_this$refs$input = this.refs.input) == null || _this$refs$input.blur();
21612
21875
  };
21613
21876
  this.getInputPaths = () => {
21614
21877
  // The widget itself is an input, so we return a single empty list to
@@ -21632,18 +21895,16 @@ class InputNumber extends React.Component {
21632
21895
  };
21633
21896
  this.simpleValidate = (rubric, onInputError) => {
21634
21897
  onInputError = onInputError || function () {};
21635
- return InputNumber.validate(this.getUserInput(), rubric, onInputError);
21898
+ return InputNumber.validate(this.getUserInput(), rubric, this.context.strings, onInputError);
21636
21899
  };
21637
21900
  this.examples = () => {
21901
+ const {
21902
+ strings
21903
+ } = this.context;
21638
21904
  const type = this.props.answerType;
21639
21905
  const forms = answerTypes[type].forms.split(/\s*,\s*/);
21640
- const examples = _.map(forms, function (form) {
21641
- // eslint-disable-next-line @babel/no-invalid-this
21642
- // @ts-expect-error - TS2683 - 'this' implicitly has type 'any' because it does not have a type annotation.
21643
- // eslint-disable-next-line @babel/no-invalid-this
21644
- return formExamples$1[form](this.props);
21645
- }, this);
21646
- return [i18n._("**Your answer should be** ")].concat(examples);
21906
+ const examples = _.map(forms, form => formExamples$1[form](this.props, strings));
21907
+ return [strings.yourAnswer].concat(examples);
21647
21908
  };
21648
21909
  }
21649
21910
  render() {
@@ -21696,7 +21957,7 @@ class InputNumber extends React.Component {
21696
21957
  linterContext: this.props.linterContext
21697
21958
  });
21698
21959
  }
21699
- static validate(state, rubric, onInputError = () => {}) {
21960
+ static validate(state, rubric, strings, onInputError = () => {}) {
21700
21961
  if (rubric.answerType == null) {
21701
21962
  rubric.answerType = "number";
21702
21963
  }
@@ -21710,7 +21971,7 @@ class InputNumber extends React.Component {
21710
21971
  inexact: rubric.inexact || undefined,
21711
21972
  maxError: rubric.maxError,
21712
21973
  forms: answerTypes[rubric.answerType].forms
21713
- });
21974
+ }, strings);
21714
21975
 
21715
21976
  // We may have received TeX; try to parse it before grading.
21716
21977
  // If `currentValue` is not TeX, this should be a no-op.
@@ -21752,6 +22013,7 @@ class InputNumber extends React.Component {
21752
22013
  return answerString;
21753
22014
  }
21754
22015
  }
22016
+ InputNumber.contextType = PerseusI18nContext;
21755
22017
  InputNumber.defaultProps = {
21756
22018
  currentValue: "",
21757
22019
  size: "normal",
@@ -21835,12 +22097,12 @@ const answerFormButtons = [{
21835
22097
  content: "\u03C0"
21836
22098
  }];
21837
22099
  const formExamples = {
21838
- integer: () => i18n._("an integer, like $6$"),
21839
- proper: form => form.simplify === "optional" ? i18n._("a *proper* fraction, like $1/2$ or $6/10$") : i18n._("a *simplified proper* fraction, like $3/5$"),
21840
- improper: form => form.simplify === "optional" ? i18n._("an *improper* fraction, like $10/7$ or $14/8$") : i18n._("a *simplified improper* fraction, like $7/4$"),
21841
- mixed: () => i18n._("a mixed number, like $1\\ 3/4$"),
21842
- decimal: () => i18n._("an *exact* decimal, like $0.75$"),
21843
- pi: () => i18n._("a multiple of pi, like $12\\ \\text{pi}$ or " + "$2/3\\ \\text{pi}$")
22100
+ integer: (form, strings) => strings.integerExample,
22101
+ proper: (form, strings) => form.simplify === "optional" ? strings.properExample : strings.simplifiedProperExample,
22102
+ improper: (form, strings) => form.simplify === "optional" ? strings.improperExample : strings.simplifiedImproperExample,
22103
+ mixed: (form, strings) => strings.mixedExample,
22104
+ decimal: (form, strings) => strings.decimalExample,
22105
+ pi: (form, strings) => strings.piExample
21844
22106
  };
21845
22107
  class NumericInput extends React.Component {
21846
22108
  constructor(...args) {
@@ -21862,13 +22124,12 @@ class NumericInput extends React.Component {
21862
22124
  };
21863
22125
  });
21864
22126
  let examples = _.map(forms, form => {
21865
- // @ts-expect-error - TS2345 - Argument of type 'PerseusNumericInputAnswerForm | { name: string; simplify: string; }' is not assignable to parameter of type 'PerseusNumericInputAnswerForm'.
21866
- return formExamples[form.name](form);
22127
+ return formExamples[form.name](form, this.context.strings);
21867
22128
  });
21868
22129
  // Ensure no duplicate tooltip text from simplified and unsimplified
21869
22130
  // versions of the same format
21870
22131
  examples = _.uniq(examples);
21871
- return [i18n._("**Your answer should be** ")].concat(examples);
22132
+ return [this.context.strings.yourAnswer].concat(examples);
21872
22133
  };
21873
22134
  this.shouldShowExamples = () => {
21874
22135
  var _this$props$answerFor2, _this$props$answerFor3;
@@ -21881,7 +22142,7 @@ class NumericInput extends React.Component {
21881
22142
  return !noFormsAccepted && !allFormsAccepted;
21882
22143
  };
21883
22144
  this.simpleValidate = rubric => {
21884
- return NumericInput.validate(this.getUserInput(), rubric);
22145
+ return NumericInput.validate(this.getUserInput(), rubric, this.context.strings);
21885
22146
  };
21886
22147
  this.focus = () => {
21887
22148
  var _this$inputRef;
@@ -21961,7 +22222,7 @@ class NumericInput extends React.Component {
21961
22222
  }
21962
22223
  return answerStrings[0];
21963
22224
  }
21964
- static validate(userInput, rubric) {
22225
+ static validate(userInput, rubric, strings) {
21965
22226
  const defaultAnswerForms = answerFormButtons.map(e => e["value"])
21966
22227
  // Don't default to validating the answer as a pi answer
21967
22228
  // if answerForm isn't set on the answer
@@ -21988,7 +22249,7 @@ class NumericInput extends React.Component {
21988
22249
  // TODO(merlob) backfill / delete
21989
22250
  maxError: answer.maxError,
21990
22251
  forms: validatorForms
21991
- });
22252
+ }, strings);
21992
22253
  };
21993
22254
 
21994
22255
  // We may have received TeX; try to parse it before grading.
@@ -22058,7 +22319,7 @@ class NumericInput extends React.Component {
22058
22319
  render() {
22059
22320
  let labelText = this.props.labelText;
22060
22321
  if (labelText == null || labelText === "") {
22061
- labelText = i18n._("Your answer:");
22322
+ labelText = this.context.strings.yourAnswerLabel;
22062
22323
  }
22063
22324
 
22064
22325
  // To right align a custom keypad we need to wrap it.
@@ -22113,6 +22374,7 @@ class NumericInput extends React.Component {
22113
22374
  // a given *problem* rather than for each possible [correct/wrong] *answer*.
22114
22375
  // When should two answers to a problem take different answer types?
22115
22376
  // See D27790 for more discussion.
22377
+ NumericInput.contextType = PerseusI18nContext;
22116
22378
  NumericInput.defaultProps = {
22117
22379
  currentValue: "",
22118
22380
  size: "normal",
@@ -22268,18 +22530,21 @@ class PassageRef extends React.Component {
22268
22530
  return !_.isEqual(this.props, nextProps) || !_.isEqual(this.state, nextState);
22269
22531
  }
22270
22532
  render() {
22533
+ const {
22534
+ strings
22535
+ } = this.context;
22271
22536
  const lineRange = this.state.lineRange;
22272
22537
  let lineRangeOutput;
22273
22538
  if (!lineRange) {
22274
- lineRangeOutput = i18n.$_("lines %(lineRange)s", {
22539
+ lineRangeOutput = strings.lineRange({
22275
22540
  lineRange: `?${EN_DASH$1}?`
22276
22541
  });
22277
22542
  } else if (lineRange[0] === lineRange[1]) {
22278
- lineRangeOutput = i18n.$_("line %(lineNumber)s", {
22279
- lineNumber: lineRange[0]
22543
+ lineRangeOutput = strings.lineNumber({
22544
+ lineNumber: String(lineRange[0])
22280
22545
  });
22281
22546
  } else {
22282
- lineRangeOutput = i18n.$_("lines %(lineRange)s", {
22547
+ lineRangeOutput = strings.lineRange({
22283
22548
  lineRange: lineRange[0] + EN_DASH$1 + lineRange[1]
22284
22549
  });
22285
22550
  }
@@ -22321,6 +22586,7 @@ class PassageRef extends React.Component {
22321
22586
  };
22322
22587
  }
22323
22588
  }
22589
+ PassageRef.contextType = PerseusI18nContext;
22324
22590
  PassageRef.defaultProps = {
22325
22591
  passageNumber: 1,
22326
22592
  referenceNumber: 1,
@@ -22684,7 +22950,7 @@ Icon.defaultProps = {
22684
22950
  color: "currentColor"
22685
22951
  };
22686
22952
 
22687
- function getA11yText(letter, checked, correct, crossedOut, showCorrectness) {
22953
+ function getA11yText(letter, checked, correct, crossedOut, showCorrectness, strings) {
22688
22954
  // There are two pieces of metadata we want to add to each a11yText:
22689
22955
  // whether the answer was checked/crossed-out/neither, and whether the
22690
22956
  // answer is correct/incorrect/not-yet-revealed.
@@ -22693,45 +22959,45 @@ function getA11yText(letter, checked, correct, crossedOut, showCorrectness) {
22693
22959
  // we've just enumerated all 9 possibilities as separate strings.
22694
22960
  if (showCorrectness && correct) {
22695
22961
  if (checked) {
22696
- return i18n._("(Choice %(letter)s, Checked, Correct)", {
22962
+ return strings.choiceCheckedCorrect({
22697
22963
  letter
22698
22964
  });
22699
22965
  }
22700
22966
  if (crossedOut) {
22701
- return i18n._("(Choice %(letter)s, Crossed out, Correct)", {
22967
+ return strings.choiceCrossedOutCorrect({
22702
22968
  letter
22703
22969
  });
22704
22970
  }
22705
- return i18n._("(Choice %(letter)s, Correct)", {
22971
+ return strings.choiceCorrect({
22706
22972
  letter
22707
22973
  });
22708
22974
  }
22709
22975
  if (showCorrectness && !correct) {
22710
22976
  if (checked) {
22711
- return i18n._("(Choice %(letter)s, Checked, Incorrect)", {
22977
+ return strings.choiceCheckedIncorrect({
22712
22978
  letter
22713
22979
  });
22714
22980
  }
22715
22981
  if (crossedOut) {
22716
- return i18n._("(Choice %(letter)s, Crossed out, Incorrect)", {
22982
+ return strings.choiceCrossedOutIncorrect({
22717
22983
  letter
22718
22984
  });
22719
22985
  }
22720
- return i18n._("(Choice %(letter)s, Incorrect)", {
22986
+ return strings.choiceIncorrect({
22721
22987
  letter
22722
22988
  });
22723
22989
  }
22724
22990
  if (checked) {
22725
- return i18n._("(Choice %(letter)s, Checked)", {
22991
+ return strings.choiceChecked({
22726
22992
  letter
22727
22993
  });
22728
22994
  }
22729
22995
  if (crossedOut) {
22730
- return i18n._("(Choice %(letter)s, Crossed out)", {
22996
+ return strings.choiceCrossedOut({
22731
22997
  letter
22732
22998
  });
22733
22999
  }
22734
- return i18n._("(Choice %(letter)s)", {
23000
+ return strings.choice({
22735
23001
  letter
22736
23002
  });
22737
23003
  }
@@ -22778,18 +23044,8 @@ const styles$z = StyleSheet.create({
22778
23044
  * "choice letter". (For example, in English, `getChoiceLetter(0)` is "A",
22779
23045
  * `getChoiceLetter(1)` is "B", etc.)
22780
23046
  */
22781
- function getChoiceLetter(pos) {
22782
- /* I18N: This is a list of single-character labels that will appear in
22783
- * front of multiple-choice options. For instance, a multiple-choice
22784
- * question with three options would display
22785
- * (A) first option
22786
- * (B) second option
22787
- * (C) third option
22788
- * There must be spaces between each of the different characters. The
22789
- * characters will show up next to options in the order that they are
22790
- * listed here. Most multiple choice questions have 5 or fewer options.
22791
- */
22792
- const lettersString = i18n._("A B C D E F G H I J K L M N O P Q R S T U V W X Y Z");
23047
+ function getChoiceLetter(pos, strings) {
23048
+ const lettersString = strings.letters;
22793
23049
  const letters = lettersString.split(" ");
22794
23050
  if (pos < letters.length) {
22795
23051
  // If the position we need is listed in the localized string, use that.
@@ -22853,7 +23109,10 @@ function ChoiceInner(props) {
22853
23109
  showCorrectness,
22854
23110
  correct
22855
23111
  } = props;
22856
- const letter = getChoiceLetter(pos);
23112
+ const {
23113
+ strings
23114
+ } = usePerseusI18n();
23115
+ const letter = getChoiceLetter(pos, strings);
22857
23116
  if (!showCorrectness) {
22858
23117
  return letter;
22859
23118
  }
@@ -22999,24 +23258,24 @@ const styles$w = StyleSheet.create({
22999
23258
  * ChoiceIcon visualizations but is meant to make the distinctions
23000
23259
  * between the states more immediately clear to users.
23001
23260
  */
23002
- function renderText(checked, correct, crossedOut) {
23261
+ function renderText(checked, correct, crossedOut, strings) {
23003
23262
  if (correct) {
23004
23263
  // For correct answers, we surface checked _or_ crossedOut state,
23005
23264
  // because any interaction with the correct answer is noteworthy!
23006
23265
  if (checked) {
23007
- return i18n._("Correct (selected)");
23266
+ return strings.correctSelected;
23008
23267
  }
23009
23268
  if (crossedOut) {
23010
- return i18n._("Correct (but you crossed it out)");
23269
+ return strings.correctCrossedOut;
23011
23270
  }
23012
- return i18n._("Correct");
23271
+ return strings.correct;
23013
23272
  }
23014
23273
  // But, for incorrect answers, we only surface checked state,
23015
23274
  // because crossing out an incorrect answer is not noteworthy.
23016
23275
  if (checked) {
23017
- return i18n._("Incorrect (selected)");
23276
+ return strings.incorrectSelected;
23018
23277
  }
23019
- return i18n._("Incorrect");
23278
+ return strings.incorrect;
23020
23279
  }
23021
23280
  const OptionStatus = function OptionStatus(props) {
23022
23281
  const {
@@ -23026,6 +23285,9 @@ const OptionStatus = function OptionStatus(props) {
23026
23285
  previouslyAnswered,
23027
23286
  reviewMode
23028
23287
  } = props;
23288
+ const {
23289
+ strings
23290
+ } = usePerseusI18n();
23029
23291
 
23030
23292
  // Option status is shown only in review mode, or for incorrectly
23031
23293
  // answered items.
@@ -23045,7 +23307,7 @@ const OptionStatus = function OptionStatus(props) {
23045
23307
  }
23046
23308
  return /*#__PURE__*/React.createElement("div", {
23047
23309
  className: css(styles$v.text, textStyle)
23048
- }, renderText(checked, correct, crossedOut));
23310
+ }, renderText(checked, correct, crossedOut, strings));
23049
23311
  };
23050
23312
  const styles$v = StyleSheet.create({
23051
23313
  text: {
@@ -23092,6 +23354,9 @@ const Choice = function Choice(props) {
23092
23354
  forwardedRef
23093
23355
  } = props;
23094
23356
  const [isInputFocused, setIsInputFocused] = useState(false);
23357
+ const {
23358
+ strings
23359
+ } = usePerseusI18n();
23095
23360
  useEffect(() => {
23096
23361
  if (isInputFocused && disabled) {
23097
23362
  setIsInputFocused(false);
@@ -23121,8 +23386,8 @@ const Choice = function Choice(props) {
23121
23386
  // However, we don't want to do this when we're in review mode in the
23122
23387
  // content library.
23123
23388
  const showDimmed = !reviewMode && apiOptions.readOnly || crossedOut;
23124
- const letter = getChoiceLetter(pos);
23125
- const a11yText = getA11yText(letter, checked, correct, crossedOut, showCorrectness);
23389
+ const letter = getChoiceLetter(pos, strings);
23390
+ const a11yText = getA11yText(letter, checked, correct, crossedOut, showCorrectness, strings);
23126
23391
  const idFactory = useUniqueIdWithMock(`choice`);
23127
23392
  const choiceId = idFactory.get(letter);
23128
23393
  return /*#__PURE__*/React.createElement("div", {
@@ -23222,15 +23487,15 @@ const Choice = function Choice(props) {
23222
23487
  content: ({
23223
23488
  close
23224
23489
  }) => /*#__PURE__*/React.createElement(PopoverContent, {
23225
- title: i18n._("Cross out"),
23226
- content: i18n._("Cross out option"),
23490
+ title: strings.crossOut,
23491
+ content: strings.crossOutOption,
23227
23492
  closeButtonVisible: true,
23228
23493
  actions: /*#__PURE__*/React.createElement(View, null, /*#__PURE__*/React.createElement(Strut, {
23229
23494
  size: spacing.medium_16
23230
23495
  }), /*#__PURE__*/React.createElement(Button, {
23231
23496
  kind: "primary",
23232
- "aria-label": i18n._("Cross out Choice %(letter)s", {
23233
- letter: getChoiceLetter(pos)
23497
+ "aria-label": strings.crossOutChoice({
23498
+ letter: getChoiceLetter(pos, strings)
23234
23499
  }),
23235
23500
  disabled: apiOptions.readOnly || reviewMode,
23236
23501
  onClick: () => {
@@ -23250,14 +23515,14 @@ const Choice = function Choice(props) {
23250
23515
  }
23251
23516
  close();
23252
23517
  }
23253
- }, crossedOut ? i18n._("Bring back") : i18n._("Cross out")))
23518
+ }, crossedOut ? strings.bringBack : strings.crossOut))
23254
23519
  })
23255
23520
  }, ({
23256
23521
  open
23257
23522
  }) => /*#__PURE__*/React.createElement(Clickable, {
23258
23523
  onClick: open,
23259
- "aria-label": i18n._(`Open menu for Choice %(letter)s`, {
23260
- letter: getChoiceLetter(pos)
23524
+ "aria-label": strings.openMenuForChoice({
23525
+ letter: getChoiceLetter(pos, strings)
23261
23526
  }),
23262
23527
  style: {
23263
23528
  alignSelf: "center",
@@ -23309,6 +23574,9 @@ const ChoiceNoneAbove = function ChoiceNoneAbove(props) {
23309
23574
  forwardedRef
23310
23575
  } = props,
23311
23576
  rest = _objectWithoutPropertiesLoose(props, _excluded$3);
23577
+ const {
23578
+ strings
23579
+ } = usePerseusI18n();
23312
23580
  const choiceProps = _extends({}, rest, {
23313
23581
  content: showContent ? content :
23314
23582
  /*#__PURE__*/
@@ -23324,7 +23592,8 @@ const ChoiceNoneAbove = function ChoiceNoneAbove(props) {
23324
23592
  // just to render a string
23325
23593
  React.createElement(Renderer, {
23326
23594
  key: "noneOfTheAboveRenderer",
23327
- content: i18n._("None of the above")
23595
+ content: strings.noneOfTheAbove,
23596
+ strings: strings
23328
23597
  })
23329
23598
  });
23330
23599
  return /*#__PURE__*/React.createElement(Choice$1, _extends({}, choiceProps, {
@@ -23344,16 +23613,16 @@ const {
23344
23613
 
23345
23614
  // exported for tests
23346
23615
 
23347
- function getInstructionsText(multipleSelect, countChoices, numCorrect) {
23616
+ function getInstructionsText(multipleSelect, countChoices, numCorrect, strings) {
23348
23617
  if (multipleSelect) {
23349
23618
  if (countChoices) {
23350
- return i18n._("Choose %(numCorrect)s answers:", {
23351
- numCorrect: numCorrect
23619
+ return strings.chooseNumAnswers({
23620
+ numCorrect: String(numCorrect)
23352
23621
  });
23353
23622
  }
23354
- return i18n._("Choose all answers that apply:");
23623
+ return strings.chooseAllAnswers;
23355
23624
  }
23356
- return i18n._("Choose 1 answer:");
23625
+ return strings.chooseOneAnswer;
23357
23626
  }
23358
23627
  const BaseRadio = function BaseRadio(props) {
23359
23628
  const {
@@ -23368,6 +23637,9 @@ const BaseRadio = function BaseRadio(props) {
23368
23637
  isLastUsedWidget,
23369
23638
  registerFocusFunction
23370
23639
  } = props;
23640
+ const {
23641
+ strings
23642
+ } = usePerseusI18n();
23371
23643
 
23372
23644
  // useEffect doesn't have previous props
23373
23645
  const prevReviewModeRubric = useRef();
@@ -23474,7 +23746,7 @@ const BaseRadio = function BaseRadio(props) {
23474
23746
  const lastChoiceHighlighted = choices[choices.length - 1].highlighted;
23475
23747
  const className = classNames("perseus-widget-radio", !editMode && "perseus-rendered-radio", css(styles$t.radio, styles$t.responsiveRadioContainer, firstChoiceHighlighted && isMobile && styles$t.radioContainerFirstHighlighted, lastChoiceHighlighted && isMobile && styles$t.radioContainerLastHighlighted));
23476
23748
  const instructionsClassName = classNames("instructions", css(styles$t.instructions, isMobile && styles$t.instructionsMobile));
23477
- const instructions = getInstructionsText(multipleSelect, countChoices, numCorrect);
23749
+ const instructions = getInstructionsText(multipleSelect, countChoices, numCorrect, strings);
23478
23750
  const responsiveClassName = css(styles$t.responsiveFieldset);
23479
23751
  const fieldset = /*#__PURE__*/React.createElement("fieldset", {
23480
23752
  className: `perseus-widget-radio-fieldset ${responsiveClassName}`
@@ -23739,7 +24011,8 @@ class Radio$1 extends React.Component {
23739
24011
  linterContext: _extends({}, this.props.linterContext, {
23740
24012
  // @ts-expect-error - TS2322 - Type '{ blockHighlight: true; contentType: string; highlightLint: boolean; paths: readonly string[]; stack: readonly string[]; }' is not assignable to type 'LinterContextProps'.
23741
24013
  blockHighlight: true
23742
- })
24014
+ }),
24015
+ strings: this.context.strings
23743
24016
  });
23744
24017
  };
23745
24018
  // When `BaseRadio`'s `onChange` handler is called, indicating a change in
@@ -23791,7 +24064,7 @@ class Radio$1 extends React.Component {
23791
24064
  return Radio$1.getUserInputFromProps(this.props);
23792
24065
  };
23793
24066
  this.simpleValidate = rubric => {
23794
- return Radio$1.validate(this.getUserInput(), rubric);
24067
+ return Radio$1.validate(this.getUserInput(), rubric, this.context.strings);
23795
24068
  };
23796
24069
  /**
23797
24070
  * Turn on rationale display for the currently selected choices. Note that
@@ -23852,7 +24125,7 @@ class Radio$1 extends React.Component {
23852
24125
  }
23853
24126
  };
23854
24127
  }
23855
- static validate(userInput, rubric) {
24128
+ static validate(userInput, rubric, strings) {
23856
24129
  const numSelected = userInput.choicesSelected.reduce((sum, selected) => {
23857
24130
  return sum + (selected ? 1 : 0);
23858
24131
  }, 0);
@@ -23865,7 +24138,7 @@ class Radio$1 extends React.Component {
23865
24138
  if (userInput.numCorrect && userInput.numCorrect > 1 && numSelected !== userInput.numCorrect) {
23866
24139
  return {
23867
24140
  type: "invalid",
23868
- message: i18n._("Please choose the correct number of answers.")
24141
+ message: strings.chooseCorrectNum
23869
24142
  };
23870
24143
  // If NOTA and some other answer are checked, ...
23871
24144
  }
@@ -23873,7 +24146,7 @@ class Radio$1 extends React.Component {
23873
24146
  if (userInput.noneOfTheAboveSelected && numSelected > 1) {
23874
24147
  return {
23875
24148
  type: "invalid",
23876
- message: i18n._("'None of the above' may not be selected " + "when other answers are selected.")
24149
+ message: strings.notNoneOfTheAbove
23877
24150
  };
23878
24151
  }
23879
24152
  const correct = userInput.choicesSelected.every((selected, i) => {
@@ -23985,6 +24258,9 @@ class Radio$1 extends React.Component {
23985
24258
  const {
23986
24259
  choices
23987
24260
  } = this.props;
24261
+ const {
24262
+ strings
24263
+ } = this.context;
23988
24264
  let choiceStates;
23989
24265
  if (this.props.static) {
23990
24266
  choiceStates = choices.map(choice => ({
@@ -24036,11 +24312,7 @@ class Radio$1 extends React.Component {
24036
24312
  }));
24037
24313
  }
24038
24314
  const choicesProp = choices.map((choice, i) => {
24039
- const content = choice.isNoneOfTheAbove && !choice.content ?
24040
- // we use i18n._ instead of $_ here because the content
24041
- // sent to a renderer needs to be a string, not a react
24042
- // node (/renderable/fragment).
24043
- i18n._("None of the above") : choice.content;
24315
+ const content = choice.isNoneOfTheAbove && !choice.content ? strings.noneOfTheAbove : choice.content;
24044
24316
  const {
24045
24317
  selected,
24046
24318
  crossedOut,
@@ -24092,6 +24364,7 @@ class Radio$1 extends React.Component {
24092
24364
  });
24093
24365
  }
24094
24366
  }
24367
+ Radio$1.contextType = PerseusI18nContext;
24095
24368
  Radio$1.defaultProps = {
24096
24369
  choices: [],
24097
24370
  multipleSelect: false,
@@ -24106,14 +24379,8 @@ const {
24106
24379
  random
24107
24380
  } = Util;
24108
24381
 
24109
- // Represents choices that we automatically re-order if encountered.
24110
- // Note: these are in the reversed (incorrect) order that we will swap, if
24111
- // found.
24112
- // Note 2: these are internationalized when compared later on.
24113
- const ReversedChoices = [[i18n._("False"), i18n._("True")], [i18n._("No"), i18n._("Yes")]];
24114
-
24115
24382
  // Transforms the choices for display.
24116
- const _choiceTransform = (widgetOptions, problemNum) => {
24383
+ const _choiceTransform = (widgetOptions, strings, problemNum) => {
24117
24384
  const _maybeRandomize = function _maybeRandomize(array) {
24118
24385
  const randomSeed = problemNum === undefined ? random : problemNum;
24119
24386
  // NOTE: `problemNum` will only be set when the radio widget is
@@ -24142,6 +24409,11 @@ const _choiceTransform = (widgetOptions, problemNum) => {
24142
24409
  return newChoices;
24143
24410
  };
24144
24411
  const enforceOrdering = choices => {
24412
+ // Represents choices that we automatically re-order if encountered.
24413
+ // Note: these are in the reversed (incorrect) order that we will swap, if
24414
+ // found.
24415
+ // Note 2: these are internationalized when compared later on.
24416
+ const ReversedChoices = [[strings.false, strings.true], [strings.no, strings.yes]];
24145
24417
  const content = choices.map(c => c.content);
24146
24418
  if (ReversedChoices.some(reversed => _.isEqual(content, reversed))) {
24147
24419
  return [choices[1], choices[0]];
@@ -24167,8 +24439,8 @@ const _choiceTransform = (widgetOptions, problemNum) => {
24167
24439
  // 1) we randomize the order first
24168
24440
  _maybeRandomize(choices)));
24169
24441
  };
24170
- const transform = (widgetOptions, problemNum) => {
24171
- const choices = _choiceTransform(widgetOptions, problemNum);
24442
+ const transform = (widgetOptions, strings, problemNum) => {
24443
+ const choices = _choiceTransform(widgetOptions, strings, problemNum);
24172
24444
  const numCorrect = _.reduce(widgetOptions.choices, function (memo, choice) {
24173
24445
  return choice.correct ? memo + 1 : memo;
24174
24446
  }, 0);
@@ -24254,7 +24526,7 @@ class Categorizer extends React.Component {
24254
24526
  return Categorizer.getUserInputFromProps(this.props);
24255
24527
  };
24256
24528
  this.simpleValidate = rubric => {
24257
- return Categorizer.validate(this.getUserInput(), rubric);
24529
+ return Categorizer.validate(this.getUserInput(), rubric, this.context.strings);
24258
24530
  };
24259
24531
  }
24260
24532
  render() {
@@ -24283,7 +24555,8 @@ class Categorizer extends React.Component {
24283
24555
  key: i
24284
24556
  }, /*#__PURE__*/React.createElement(Renderer, {
24285
24557
  content: category,
24286
- linterContext: this.props.linterContext
24558
+ linterContext: this.props.linterContext,
24559
+ strings: this.context.strings
24287
24560
  }));
24288
24561
  }))), /*#__PURE__*/React.createElement("tbody", null, indexedItems.map(indexedItem => {
24289
24562
  const item = indexedItem[0];
@@ -24295,7 +24568,8 @@ class Categorizer extends React.Component {
24295
24568
  // @ts-expect-error - TS2322 - Type 'string | number' is not assignable to type 'string | undefined'.
24296
24569
  , {
24297
24570
  content: item,
24298
- linterContext: this.props.linterContext
24571
+ linterContext: this.props.linterContext,
24572
+ strings: this.context.strings
24299
24573
  })), self.props.categories.map((catName, catNum) => {
24300
24574
  const selected = self.props.values[itemNum] === catNum;
24301
24575
  return /*#__PURE__*/React.createElement("td", {
@@ -24338,7 +24612,7 @@ class Categorizer extends React.Component {
24338
24612
  this.change("values", values);
24339
24613
  this.props.trackInteraction();
24340
24614
  }
24341
- static validate(userInput, rubric) {
24615
+ static validate(userInput, rubric, strings) {
24342
24616
  let completed = true;
24343
24617
  let allCorrect = true;
24344
24618
  rubric.values.forEach((value, i) => {
@@ -24352,7 +24626,7 @@ class Categorizer extends React.Component {
24352
24626
  if (!completed) {
24353
24627
  return {
24354
24628
  type: "invalid",
24355
- message: i18n._("Make sure you select something for every row.")
24629
+ message: strings.invalidSelection
24356
24630
  };
24357
24631
  }
24358
24632
  return {
@@ -24368,6 +24642,7 @@ class Categorizer extends React.Component {
24368
24642
  }
24369
24643
 
24370
24644
  // TODO(benkomalo): inject page-margin into Perseus instead of hardcoding.
24645
+ Categorizer.contextType = PerseusI18nContext;
24371
24646
  Categorizer.defaultProps = {
24372
24647
  items: [],
24373
24648
  categories: [],
@@ -24693,7 +24968,8 @@ class Definition extends React.Component {
24693
24968
  }, /*#__PURE__*/React.createElement(Renderer, {
24694
24969
  apiOptions: this.props.apiOptions,
24695
24970
  content: this.props.definition,
24696
- widgets: this.props.widgets
24971
+ widgets: this.props.widgets,
24972
+ strings: this.context.strings
24697
24973
  })),
24698
24974
  opened: activeDefinitionId === this.props.widgetId,
24699
24975
  onClose: () => setActiveDefinitionId(null),
@@ -24715,6 +24991,7 @@ class Definition extends React.Component {
24715
24991
  }, this.props.togglePrompt))));
24716
24992
  }
24717
24993
  }
24994
+ Definition.contextType = PerseusI18nContext;
24718
24995
  Definition.defaultProps = {
24719
24996
  togglePrompt: "define me",
24720
24997
  definition: "definition goes here"
@@ -24765,12 +25042,13 @@ class DeprecatedStandin extends React__default.Component {
24765
25042
  paddingBottom: 8
24766
25043
  }
24767
25044
  }, /*#__PURE__*/React__default.createElement(Banner, {
24768
- text: i18n._("Sorry, this part of the question is no longer available. 😅 Don't worry, you won't be graded on this part. Keep going!"),
25045
+ text: this.context.strings.deprecatedStandin,
24769
25046
  kind: "info",
24770
25047
  layout: "full-width"
24771
25048
  }));
24772
25049
  }
24773
25050
  }
25051
+ DeprecatedStandin.contextType = PerseusI18nContext;
24774
25052
  var DeprecatedStandin$1 = {
24775
25053
  name: "deprecated-standin",
24776
25054
  displayName: "Deprecated Standin",
@@ -24965,10 +25243,12 @@ class Explanation extends React.Component {
24965
25243
  apiOptions: this.props.apiOptions,
24966
25244
  content: this.props.explanation,
24967
25245
  widgets: this.props.widgets,
24968
- linterContext: this.props.linterContext
25246
+ linterContext: this.props.linterContext,
25247
+ strings: this.context.strings
24969
25248
  })))));
24970
25249
  }
24971
25250
  }
25251
+ Explanation.contextType = PerseusI18nContext;
24972
25252
  Explanation.defaultProps = {
24973
25253
  showPrompt: "Explain",
24974
25254
  hidePrompt: "Hide explanation",
@@ -25036,6 +25316,13 @@ class GradedGroupAnswerBar extends React.Component {
25036
25316
  onCheckAnswer,
25037
25317
  onNextQuestion
25038
25318
  } = this.props;
25319
+ const {
25320
+ keepTrying,
25321
+ tryAgain,
25322
+ check,
25323
+ correctExcited,
25324
+ nextQuestion
25325
+ } = this.context.strings;
25039
25326
  const answerBarStyle = _extends({}, styles$o.answerBar, {
25040
25327
  backgroundColor: answerBarState === "CORRECT" ? color.offWhite : "white",
25041
25328
  // Center the "Correct!" message only when there's no next question
@@ -25049,10 +25336,10 @@ class GradedGroupAnswerBar extends React.Component {
25049
25336
  style: {
25050
25337
  marginLeft: 8
25051
25338
  }
25052
- }, i18n._("Keep trying"))) : /*#__PURE__*/React.createElement("span", null); // empty span keeps the button on the right side
25339
+ }, keepTrying)) : /*#__PURE__*/React.createElement("span", null); // empty span keeps the button on the right side
25053
25340
 
25054
25341
  if (answerBarState !== "CORRECT") {
25055
- const buttonLabel = answerBarState === "INCORRECT" ? i18n._("Try again") : i18n._("Check");
25342
+ const buttonLabel = answerBarState === "INCORRECT" ? tryAgain : check;
25056
25343
  return /*#__PURE__*/React.createElement("div", {
25057
25344
  style: answerBarStyle
25058
25345
  }, message, /*#__PURE__*/React.createElement(Button, {
@@ -25075,15 +25362,16 @@ class GradedGroupAnswerBar extends React.Component {
25075
25362
  }
25076
25363
  }))), /*#__PURE__*/React.createElement("span", {
25077
25364
  role: "alert",
25078
- "aria-label": i18n._("Correct!"),
25365
+ "aria-label": correctExcited,
25079
25366
  style: {
25080
25367
  marginLeft: 8
25081
25368
  }
25082
- }, i18n._("Correct!"))), onNextQuestion && /*#__PURE__*/React.createElement(Button, {
25369
+ }, correctExcited)), onNextQuestion && /*#__PURE__*/React.createElement(Button, {
25083
25370
  onClick: onNextQuestion
25084
- }, i18n._("Next question")));
25371
+ }, nextQuestion));
25085
25372
  }
25086
25373
  }
25374
+ GradedGroupAnswerBar.contextType = PerseusI18nContext;
25087
25375
  const fontSize = 17;
25088
25376
  const styles$o = {
25089
25377
  answerBar: {
@@ -25137,10 +25425,6 @@ const getNextState = (currentState, answerable) => {
25137
25425
  }
25138
25426
  };
25139
25427
 
25140
- // Prepended to all invalid messages to make the widget messages a bit clearer
25141
- const INVALID_MESSAGE_PREFIX = i18n._("We couldn't grade your answer.");
25142
- const DEFAULT_INVALID_MESSAGE = i18n._("It looks like you left something blank or ") + i18n._("entered in an invalid answer.");
25143
-
25144
25428
  // exports has no 'transform'
25145
25429
 
25146
25430
  // A Graded Group is more or less a Group widget that displays a check
@@ -25192,8 +25476,13 @@ class GradedGroup extends React.Component {
25192
25476
  // eslint-disable-next-line react/no-string-refs
25193
25477
  // @ts-expect-error - TS2339 - Property 'score' does not exist on type 'ReactInstance'.
25194
25478
  const score = this.refs.renderer.score();
25479
+ const {
25480
+ INVALID_MESSAGE_PREFIX,
25481
+ DEFAULT_INVALID_MESSAGE_1,
25482
+ DEFAULT_INVALID_MESSAGE_2
25483
+ } = this.context.strings;
25195
25484
  const status = score.type === "points" ? score.total === score.earned ? GRADING_STATUSES.correct : GRADING_STATUSES.incorrect : GRADING_STATUSES.invalid;
25196
- const message = score.type === "points" ? score.message || "" : score.message ? `${INVALID_MESSAGE_PREFIX} ${score.message}` : `${INVALID_MESSAGE_PREFIX} ${DEFAULT_INVALID_MESSAGE}`;
25485
+ const message = score.type === "points" ? score.message || "" : score.message ? `${INVALID_MESSAGE_PREFIX} ${score.message}` : `${INVALID_MESSAGE_PREFIX} ${DEFAULT_INVALID_MESSAGE_1}${DEFAULT_INVALID_MESSAGE_2}`;
25197
25486
  this.setState({
25198
25487
  status: status,
25199
25488
  message: message,
@@ -25258,8 +25547,7 @@ class GradedGroup extends React.Component {
25258
25547
  color: "#526f03"
25259
25548
  }
25260
25549
  }));
25261
- // @ts-expect-error - TS2322 - Type 'string' is not assignable to type 'null'.
25262
- gradeStatus = i18n._("Correct");
25550
+ gradeStatus = this.context.strings.correct;
25263
25551
  } else if (this.state.status === GRADING_STATUSES.incorrect) {
25264
25552
  // TODO(jeremy): update to a WB colour
25265
25553
  // @ts-expect-error - TS2322 - Type 'Element' is not assignable to type 'null'.
@@ -25268,8 +25556,7 @@ class GradedGroup extends React.Component {
25268
25556
  color: "#ff5454"
25269
25557
  }
25270
25558
  }));
25271
- // @ts-expect-error - TS2322 - Type 'string' is not assignable to type 'null'.
25272
- gradeStatus = i18n._("Incorrect");
25559
+ gradeStatus = this.context.strings.incorrect;
25273
25560
  }
25274
25561
  const mobileClass = this.props.inGradedGroupSet ? css(styles$n.gradedGroupInSet) : css(styles$n.gradedGroup);
25275
25562
  const classes = classNames({
@@ -25298,7 +25585,8 @@ class GradedGroup extends React.Component {
25298
25585
  readOnly
25299
25586
  }),
25300
25587
  onInteractWithWidget: this._onInteractWithWidget,
25301
- linterContext: this.props.linterContext
25588
+ linterContext: this.props.linterContext,
25589
+ strings: this.context.strings
25302
25590
  })), !apiOptions.isMobile && icon && /*#__PURE__*/React.createElement("div", {
25303
25591
  className: "group-icon"
25304
25592
  }, icon), !apiOptions.isMobile && gradeStatus && /*#__PURE__*/React.createElement("div", {
@@ -25312,14 +25600,14 @@ class GradedGroup extends React.Component {
25312
25600
  kind: "secondary",
25313
25601
  disabled: this.props.apiOptions.readOnly,
25314
25602
  onClick: this._checkAnswer
25315
- }, i18n._("Check")), !apiOptions.isMobile && isCorrect && this.props.onNextQuestion && /*#__PURE__*/React.createElement(Button, {
25603
+ }, this.context.strings.check), !apiOptions.isMobile && isCorrect && this.props.onNextQuestion && /*#__PURE__*/React.createElement(Button, {
25316
25604
  kind: "secondary",
25317
25605
  disabled: this.props.apiOptions.readOnly,
25318
25606
  onClick: this.props.onNextQuestion,
25319
25607
  style: {
25320
25608
  marginLeft: 5
25321
25609
  }
25322
- }, i18n._("Next question")), this.props.hint && this.props.hint.content && (this.state.showHint ? /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement("button", {
25610
+ }, this.context.strings.nextQuestion), this.props.hint && this.props.hint.content && (this.state.showHint ? /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement("button", {
25323
25611
  // @ts-expect-error - TS2322 - Type 'string' is not assignable to type 'number | undefined'.
25324
25612
  tabIndex: "0",
25325
25613
  className: css(styles$n.explanationTitle),
@@ -25333,11 +25621,12 @@ class GradedGroup extends React.Component {
25333
25621
  showHint: false
25334
25622
  });
25335
25623
  }
25336
- }, i18n._("Hide explanation")), /*#__PURE__*/React.createElement(Renderer, _extends({}, this.props.hint, {
25624
+ }, this.context.strings.hideExplanation), /*#__PURE__*/React.createElement(Renderer, _extends({}, this.props.hint, {
25337
25625
  // eslint-disable-next-line react/no-string-refs
25338
25626
  ref: "hints-renderer",
25339
25627
  apiOptions: apiOptions,
25340
- linterContext: this.props.linterContext
25628
+ linterContext: this.props.linterContext,
25629
+ strings: this.context.strings
25341
25630
  }))) :
25342
25631
  /*#__PURE__*/
25343
25632
  // Not using Button here bc the styles won't work.
@@ -25355,7 +25644,7 @@ class GradedGroup extends React.Component {
25355
25644
  });
25356
25645
  },
25357
25646
  className: css(styles$n.showHintLink)
25358
- }, i18n._("Explain"))), apiOptions.isMobile && answerBarState !== "HIDDEN" && /*#__PURE__*/React.createElement(GradedGroupAnswerBar, {
25647
+ }, this.context.strings.explain)), apiOptions.isMobile && answerBarState !== "HIDDEN" && /*#__PURE__*/React.createElement(GradedGroupAnswerBar, {
25359
25648
  apiOptions: apiOptions,
25360
25649
  answerBarState: answerBarState,
25361
25650
  onCheckAnswer: this._checkAnswer,
@@ -25363,6 +25652,7 @@ class GradedGroup extends React.Component {
25363
25652
  }));
25364
25653
  }
25365
25654
  }
25655
+ GradedGroup.contextType = PerseusI18nContext;
25366
25656
  GradedGroup.defaultProps = {
25367
25657
  title: "",
25368
25658
  content: "",
@@ -25456,7 +25746,7 @@ class Indicators extends React.Component {
25456
25746
  key: title
25457
25747
  }, /*#__PURE__*/React.createElement(Clickable, {
25458
25748
  role: "button",
25459
- "aria-label": i18n._("Skip to %(title)s", {
25749
+ "aria-label": this.context.strings.skipToTitle({
25460
25750
  title
25461
25751
  }),
25462
25752
  style: styles$m.indicatorButton,
@@ -25472,12 +25762,10 @@ class Indicators extends React.Component {
25472
25762
  style: styles$m.indicatorDotActive
25473
25763
  }, /*#__PURE__*/React.createElement("span", {
25474
25764
  className: css(a11y.srOnly)
25475
- }, i18n._("Current"))))))));
25765
+ }, this.context.strings.current)))))));
25476
25766
  }
25477
25767
  }
25478
-
25479
- // no transform
25480
-
25768
+ Indicators.contextType = PerseusI18nContext; // no transform
25481
25769
  // TODO(jared): find a better name for this :) and for GradedGroup; the names
25482
25770
  // are currently a little confusing.
25483
25771
  class GradedGroupSet extends React.Component {
@@ -25553,7 +25841,7 @@ class GradedGroupSet extends React.Component {
25553
25841
  }
25554
25842
  const currentGroup = this.props.gradedGroups[this.state.currentGroup];
25555
25843
  if (!currentGroup) {
25556
- return /*#__PURE__*/React.createElement("span", null, i18n.doNotTranslate("No current group..."));
25844
+ return /*#__PURE__*/React.createElement("span", null, "No current group...");
25557
25845
  }
25558
25846
  const numGroups = this.props.gradedGroups.length;
25559
25847
  const handleNextQuestion = this.state.currentGroup < numGroups - 1 ? this.handleNextQuestion : null;
@@ -26848,7 +27136,8 @@ class Group extends React.Component {
26848
27136
  findExternalWidgets: this.props.findWidgets,
26849
27137
  reviewMode: !!this.props.reviewModeRubric,
26850
27138
  onInteractWithWidget: onInteractWithWidget,
26851
- linterContext: this.props.linterContext
27139
+ linterContext: this.props.linterContext,
27140
+ strings: this.context.strings
26852
27141
  }), this.props.icon &&
26853
27142
  /*#__PURE__*/
26854
27143
  // @ts-expect-error - TS2339 - Property 'icon' does not exist on type 'Readonly<Props> & Readonly<{ children?: ReactNode; }>'.
@@ -26857,6 +27146,7 @@ class Group extends React.Component {
26857
27146
  }, this.props.icon));
26858
27147
  }
26859
27148
  }
27149
+ Group.contextType = PerseusI18nContext;
26860
27150
  Group.defaultProps = {
26861
27151
  content: "",
26862
27152
  widgets: {},
@@ -27122,7 +27412,8 @@ class ImageWidget extends React.Component {
27122
27412
  }, /*#__PURE__*/React.createElement(Renderer, {
27123
27413
  content: this.props.alt,
27124
27414
  apiOptions: apiOptions,
27125
- linterContext: this.props.linterContext
27415
+ linterContext: this.props.linterContext,
27416
+ strings: this.context.strings
27126
27417
  }));
27127
27418
  }
27128
27419
 
@@ -27182,7 +27473,8 @@ class ImageWidget extends React.Component {
27182
27473
  }, /*#__PURE__*/React.createElement(Renderer, {
27183
27474
  content: _title + this.props.caption,
27184
27475
  apiOptions: apiOptions,
27185
- linterContext: this.props.linterContext
27476
+ linterContext: this.props.linterContext,
27477
+ strings: this.context.strings
27186
27478
  })));
27187
27479
  }
27188
27480
  return /*#__PURE__*/React.createElement("figure", {
@@ -27200,7 +27492,8 @@ class ImageWidget extends React.Component {
27200
27492
  }, /*#__PURE__*/React.createElement(Renderer, {
27201
27493
  content: this.props.title,
27202
27494
  apiOptions: apiOptions,
27203
- linterContext: this.props.linterContext
27495
+ linterContext: this.props.linterContext,
27496
+ strings: this.context.strings
27204
27497
  }));
27205
27498
  }
27206
27499
  if (this.props.caption) {
@@ -27212,7 +27505,8 @@ class ImageWidget extends React.Component {
27212
27505
  }, /*#__PURE__*/React.createElement(Renderer, {
27213
27506
  content: this.props.caption,
27214
27507
  apiOptions: apiOptions,
27215
- linterContext: this.props.linterContext
27508
+ linterContext: this.props.linterContext,
27509
+ strings: this.context.strings
27216
27510
  }));
27217
27511
  }
27218
27512
  return /*#__PURE__*/React.createElement("figure", {
@@ -27231,6 +27525,7 @@ class ImageWidget extends React.Component {
27231
27525
  };
27232
27526
  }
27233
27527
  }
27528
+ ImageWidget.contextType = PerseusI18nContext;
27234
27529
  ImageWidget.defaultProps = {
27235
27530
  alignment: DEFAULT_ALIGNMENT,
27236
27531
  title: "",
@@ -28163,6 +28458,123 @@ Graph.defaultProps = {
28163
28458
  isMobile: false
28164
28459
  };
28165
28460
 
28461
+ // TODO(FEI-4010): Remove `Perseus` prefix for all types here
28462
+ // TODO(FEI-4011): Use types generated by https://github.com/jaredly/generate-perseus-flowtypes
28463
+
28464
+ // Range is replaced within this file with Interval, but it is used elsewhere
28465
+ // and exported from the package, so we need to keep it around.
28466
+ // TODO(FEI-5054): Figure out how to get global .d.ts files working with monorepos
28467
+ const ItemExtras = [
28468
+ // The user might benefit from using a Scientific Calculator. Provided on Khan Academy when true
28469
+ "calculator",
28470
+ // The user might benefit from using a statistics Chi Squared Table like https://people.richland.edu/james/lecture/m170/tbl-chi.html
28471
+ "chi2Table",
28472
+ // The user might benefit from a monthly payments calculator. Provided on Khan Academy when true
28473
+ "financialCalculatorMonthlyPayment",
28474
+ // The user might benefit from a total amount calculator. Provided on Khan Academy when true
28475
+ "financialCalculatorTotalAmount",
28476
+ // The user might benefit from a time to pay off calculator. Provided on Khan Academy when true
28477
+ "financialCalculatorTimeToPayOff",
28478
+ // The user might benefit from using a Periodic Table of Elements. Provided on Khan Academy when true
28479
+ "periodicTable",
28480
+ // The user might benefit from using a Periodic Table of Elements with key. Provided on Khan Academy when true
28481
+ "periodicTableWithKey",
28482
+ // The user might benefit from using a statistics T Table like https://www.statisticshowto.com/tables/t-distribution-table/
28483
+ "tTable",
28484
+ // The user might benefit from using a statistics Z Table like https://www.ztable.net/
28485
+ "zTable"];
28486
+
28487
+ // prettier-ignore
28488
+
28489
+ // prettier-ignore
28490
+
28491
+ // prettier-ignore
28492
+
28493
+ // prettier-ignore
28494
+
28495
+ // prettier-ignore
28496
+
28497
+ // prettier-ignore
28498
+
28499
+ // prettier-ignore
28500
+
28501
+ // prettier-ignore
28502
+
28503
+ // prettier-ignore
28504
+
28505
+ // prettier-ignore
28506
+
28507
+ // prettier-ignore
28508
+
28509
+ // prettier-ignore
28510
+
28511
+ // prettier-ignore
28512
+
28513
+ // prettier-ignore
28514
+
28515
+ // prettier-ignore
28516
+
28517
+ // prettier-ignore
28518
+
28519
+ // prettier-ignore
28520
+
28521
+ // prettier-ignore
28522
+
28523
+ // prettier-ignore
28524
+
28525
+ // prettier-ignore
28526
+
28527
+ // prettier-ignore
28528
+
28529
+ // prettier-ignore
28530
+
28531
+ // prettier-ignore
28532
+
28533
+ // prettier-ignore
28534
+
28535
+ // prettier-ignore
28536
+
28537
+ // prettier-ignore
28538
+
28539
+ // prettier-ignore
28540
+
28541
+ // prettier-ignore
28542
+
28543
+ // prettier-ignore
28544
+
28545
+ // prettier-ignore
28546
+
28547
+ // prettier-ignore
28548
+
28549
+ // prettier-ignore
28550
+
28551
+ // prettier-ignore
28552
+
28553
+ // prettier-ignore
28554
+
28555
+ // prettier-ignore
28556
+
28557
+ // prettier-ignore
28558
+
28559
+ //prettier-ignore
28560
+
28561
+ // A background image applied to various widgets.
28562
+
28563
+ const PerseusExpressionAnswerFormConsidered = ["correct", "wrong", "ungraded"];
28564
+ const lockedFigureColors = {
28565
+ blue: "#3D7586",
28566
+ green: "#447A53",
28567
+ gray: "#5D5F66",
28568
+ grayH: "#3B3D45",
28569
+ grayI: "#21242C",
28570
+ purple: "#594094",
28571
+ purpleD: "#8351E8",
28572
+ pink: "#B25071",
28573
+ orange: "#946700",
28574
+ red: "#D92916"
28575
+ };
28576
+ const plotterPlotTypes = ["bar", "line", "pic", "histogram", "dotplot"];
28577
+
28166
28578
  const LockedLine = props => {
28167
28579
  const {
28168
28580
  color: color$1,
@@ -28179,7 +28591,7 @@ const LockedLine = props => {
28179
28591
  line = /*#__PURE__*/React.createElement(LineType, {
28180
28592
  point1: point1.coord,
28181
28593
  point2: point2.coord,
28182
- color: color[color$1],
28594
+ color: lockedFigureColors[color$1],
28183
28595
  style: lineStyle
28184
28596
  });
28185
28597
  } else {
@@ -28193,8 +28605,8 @@ const LockedLine = props => {
28193
28605
  y: point1.coord[1],
28194
28606
  svgCircleProps: {
28195
28607
  style: {
28196
- fill: point1.filled ? color[point1.color] : color.white,
28197
- stroke: color[point1.color],
28608
+ fill: point1.filled ? lockedFigureColors[point1.color] : color.white,
28609
+ stroke: lockedFigureColors[point1.color],
28198
28610
  strokeWidth: spacing.xxxxSmall_2
28199
28611
  }
28200
28612
  }
@@ -28203,8 +28615,8 @@ const LockedLine = props => {
28203
28615
  y: point2.coord[1],
28204
28616
  svgCircleProps: {
28205
28617
  style: {
28206
- fill: point2.filled ? color[point2.color] : color.white,
28207
- stroke: color[point2.color],
28618
+ fill: point2.filled ? lockedFigureColors[point2.color] : color.white,
28619
+ stroke: lockedFigureColors[point2.color],
28208
28620
  strokeWidth: spacing.xxxxSmall_2
28209
28621
  }
28210
28622
  }
@@ -28223,8 +28635,8 @@ const LockedPoint = props => {
28223
28635
  y: y,
28224
28636
  svgCircleProps: {
28225
28637
  style: {
28226
- fill: filled ? color[color$1] : color.white,
28227
- stroke: color[color$1],
28638
+ fill: filled ? lockedFigureColors[color$1] : color.white,
28639
+ stroke: lockedFigureColors[color$1],
28228
28640
  strokeWidth: spacing.xxxxSmall_2
28229
28641
  }
28230
28642
  }
@@ -28459,6 +28871,7 @@ function scaleCommandBy(scaleFactor) {
28459
28871
  // don't want that.
28460
28872
  const arrowPath = pathBuilder().move(-3, 4).curve(-2.75, 2.5, 0, 0.25, 0.75, 0).curve(0, -0.25, -2.75, -2.5, -3, -4).scale(1.4).build();
28461
28873
  function Arrowhead(props) {
28874
+ var _props$color;
28462
28875
  const [point] = useTransform([props.x, props.y]);
28463
28876
  return /*#__PURE__*/React.createElement("g", {
28464
28877
  transform: `translate(${point[0]} ${point[1]}) rotate(${-props.angle})`
@@ -28468,7 +28881,7 @@ function Arrowhead(props) {
28468
28881
  d: arrowPath,
28469
28882
  fill: "none",
28470
28883
  style: {
28471
- stroke: "inherit"
28884
+ stroke: (_props$color = props.color) != null ? _props$color : "inherit"
28472
28885
  },
28473
28886
  strokeLinejoin: "round",
28474
28887
  strokeLinecap: "round"
@@ -28577,7 +28990,8 @@ const MovableLine = props => {
28577
28990
  const defaultGraphConfig = {
28578
28991
  range: [[0, 1], [0, 1]],
28579
28992
  snapStep: [1, 1],
28580
- markings: "none"
28993
+ markings: "none",
28994
+ showTooltips: false
28581
28995
  };
28582
28996
  const GraphConfigContext = /*#__PURE__*/createContext(defaultGraphConfig);
28583
28997
  function useGraphConfig() {
@@ -28588,10 +29002,12 @@ function useGraphConfig() {
28588
29002
  // graph.
28589
29003
  const hitboxSizePx = 48;
28590
29004
  const StyledMovablePoint = props => {
29005
+ var _Object$entries$find$, _Object$entries$find;
28591
29006
  const {
28592
29007
  range,
28593
29008
  snapStep,
28594
- markings
29009
+ markings,
29010
+ showTooltips
28595
29011
  } = useGraphConfig();
28596
29012
  const hitboxRef = useRef(null);
28597
29013
  const {
@@ -28599,6 +29015,10 @@ const StyledMovablePoint = props => {
28599
29015
  onMove,
28600
29016
  color: color$1 = color.blue
28601
29017
  } = props;
29018
+
29019
+ // WB Tooltip requires a color name for the background color.
29020
+ // Since the color in props is a hex value, a reverse lookup is needed.
29021
+ const wbColorName = (_Object$entries$find$ = (_Object$entries$find = Object.entries(color).find(([_, value]) => value === color$1)) == null ? void 0 : _Object$entries$find[0]) != null ? _Object$entries$find$ : "blue";
28602
29022
  const {
28603
29023
  dragging
28604
29024
  } = useMovable({
@@ -28616,19 +29036,20 @@ const StyledMovablePoint = props => {
28616
29036
  const [[_, horizontalStartY]] = useTransform([0, yMin]);
28617
29037
  const [[__, horizontalEndY]] = useTransform([0, yMax]);
28618
29038
  const showHairlines = dragging && markings !== "none";
28619
- return /*#__PURE__*/React.createElement(React.Fragment, null, showHairlines && /*#__PURE__*/React.createElement("g", null, /*#__PURE__*/React.createElement("line", {
29039
+ const hairlines = /*#__PURE__*/React.createElement("g", null, /*#__PURE__*/React.createElement("line", {
28620
29040
  x1: verticalStartX,
28621
29041
  y1: y,
28622
29042
  x2: verticalEndX,
28623
29043
  y2: y,
28624
- stroke: color.green
29044
+ stroke: color$1
28625
29045
  }), /*#__PURE__*/React.createElement("line", {
28626
29046
  x1: x,
28627
29047
  y1: horizontalStartY,
28628
29048
  x2: x,
28629
29049
  y2: horizontalEndY,
28630
- stroke: color.green
28631
- })), /*#__PURE__*/React.createElement("g", {
29050
+ stroke: color$1
29051
+ }));
29052
+ const svgForPoint = /*#__PURE__*/React.createElement("g", {
28632
29053
  ref: hitboxRef,
28633
29054
  className: pointClasses,
28634
29055
  tabIndex: 0,
@@ -28661,7 +29082,15 @@ const StyledMovablePoint = props => {
28661
29082
  fill: color$1
28662
29083
  },
28663
29084
  "data-testid": "movable-point__center"
28664
- })));
29085
+ }));
29086
+ return /*#__PURE__*/React.createElement(React.Fragment, null, showHairlines && hairlines, showTooltips ? /*#__PURE__*/React.createElement(Tooltip$1, {
29087
+ autoUpdate: true,
29088
+ backgroundColor: wbColorName,
29089
+ content: `(${point[0]}, ${point[1]})`,
29090
+ contentStyle: {
29091
+ color: "white"
29092
+ }
29093
+ }, svgForPoint) : svgForPoint);
28665
29094
  };
28666
29095
 
28667
29096
  const SegmentGraph = props => {
@@ -29047,22 +29476,27 @@ function AxisArrows() {
29047
29476
  const {
29048
29477
  range: [[xMin, xMax], [yMin, yMax]]
29049
29478
  } = useGraphConfig();
29479
+ const axisColor = "var(--mafs-fg)";
29050
29480
  return /*#__PURE__*/React__default.createElement(React__default.Fragment, null, /*#__PURE__*/React__default.createElement(Arrowhead, {
29481
+ color: axisColor,
29482
+ x: xMin,
29483
+ y: 0,
29484
+ angle: 180
29485
+ }), /*#__PURE__*/React__default.createElement(Arrowhead, {
29486
+ color: axisColor,
29051
29487
  x: xMax,
29052
29488
  y: 0,
29053
29489
  angle: 0
29054
29490
  }), /*#__PURE__*/React__default.createElement(Arrowhead, {
29491
+ color: axisColor,
29055
29492
  x: 0,
29056
29493
  y: yMin,
29057
- angle: 90
29058
- }), /*#__PURE__*/React__default.createElement(Arrowhead, {
29059
- x: xMin,
29060
- y: 0,
29061
- angle: 180
29494
+ angle: 270
29062
29495
  }), /*#__PURE__*/React__default.createElement(Arrowhead, {
29496
+ color: axisColor,
29063
29497
  x: 0,
29064
29498
  y: yMax,
29065
- angle: 270
29499
+ angle: 90
29066
29500
  }));
29067
29501
  }
29068
29502
 
@@ -29188,7 +29622,7 @@ const Grid = props => {
29188
29622
  return props.markings === "none" ? null : /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(Coordinates.Cartesian, {
29189
29623
  xAxis: axisOptions(props, 0),
29190
29624
  yAxis: axisOptions(props, 1)
29191
- }), props.markings === "graph" && /*#__PURE__*/React.createElement(AxisArrows, null),
29625
+ }),
29192
29626
  // Only render the axis ticks and arrows if the markings are set to a full "graph"
29193
29627
  props.markings === "graph" && /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(AxisTicks, {
29194
29628
  range: props.range,
@@ -29587,7 +30021,7 @@ class NumberInput extends React.Component {
29587
30021
  const placeholder = this.props.placeholder;
29588
30022
  return _.isFinite(placeholder) ? +placeholder : null;
29589
30023
  }
29590
- const result = firstNumericalParse(value);
30024
+ const result = firstNumericalParse(value, this.context.strings);
29591
30025
  return _.isFinite(result) ? result : this.props.value;
29592
30026
  };
29593
30027
  /* Set text input focus to this input */
@@ -29617,7 +30051,7 @@ class NumberInput extends React.Component {
29617
30051
  if (value == null) {
29618
30052
  return true;
29619
30053
  }
29620
- const val = firstNumericalParse(value);
30054
+ const val = firstNumericalParse(value, this.context.strings);
29621
30055
  const checkValidity = this.props.checkValidity;
29622
30056
  return _.isFinite(val) && checkValidity(val);
29623
30057
  };
@@ -29713,6 +30147,7 @@ class NumberInput extends React.Component {
29713
30147
  }
29714
30148
  }
29715
30149
  }
30150
+ NumberInput.contextType = PerseusI18nContext;
29716
30151
  NumberInput.propTypes = {
29717
30152
  value: PropTypes.number,
29718
30153
  format: PropTypes.string,
@@ -30448,7 +30883,8 @@ const MafsGraph = props => {
30448
30883
  value: {
30449
30884
  range: state.range,
30450
30885
  snapStep: state.snapStep,
30451
- markings: props.markings
30886
+ markings: props.markings,
30887
+ showTooltips: !!props.showTooltips
30452
30888
  }
30453
30889
  }, /*#__PURE__*/React.createElement(View, {
30454
30890
  style: {
@@ -31710,9 +32146,9 @@ class LegacyInteractiveGraph extends React.Component {
31710
32146
  // isClickToAddPoints() only applies to points and polygons
31711
32147
  if (this.isClickToAddPoints() && this.state.shouldShowInstructions) {
31712
32148
  if (this.props.graph.type === "point") {
31713
- instructions = i18n._("Click to add points");
32149
+ instructions = this.context.strings.addPoints;
31714
32150
  } else if (this.props.graph.type === "polygon") {
31715
- instructions = i18n._("Click to add vertices");
32151
+ instructions = this.context.strings.addVertices;
31716
32152
  }
31717
32153
  } else {
31718
32154
  instructions = undefined;
@@ -31752,6 +32188,7 @@ class LegacyInteractiveGraph extends React.Component {
31752
32188
  }));
31753
32189
  }
31754
32190
  }
32191
+ LegacyInteractiveGraph.contextType = PerseusI18nContext;
31755
32192
  LegacyInteractiveGraph.defaultProps = {
31756
32193
  labels: ["x", "y"],
31757
32194
  range: [[-10, 10], [-10, 10]],
@@ -31798,7 +32235,8 @@ class InteractiveGraph extends React.Component {
31798
32235
  ref: this.mafsRef,
31799
32236
  gridStep: gridStep,
31800
32237
  snapStep: snapStep,
31801
- box: box
32238
+ box: box,
32239
+ showTooltips: !!this.props.showTooltips
31802
32240
  }));
31803
32241
  }
31804
32242
  return /*#__PURE__*/React.createElement(LegacyInteractiveGraph, _extends({
@@ -33340,6 +33778,9 @@ var globalStyles = /*#__PURE__*/Object.freeze({
33340
33778
  });
33341
33779
 
33342
33780
  const AnswerChoices = props => {
33781
+ const {
33782
+ strings
33783
+ } = usePerseusI18n();
33343
33784
  const onAnswerChange = selected => {
33344
33785
  const {
33345
33786
  choices,
@@ -33357,6 +33798,7 @@ const AnswerChoices = props => {
33357
33798
  value: content,
33358
33799
  label: /*#__PURE__*/React.createElement(Renderer, {
33359
33800
  content: content,
33801
+ strings: strings,
33360
33802
  inline: true
33361
33803
  })
33362
33804
  }));
@@ -33386,14 +33828,13 @@ const AnswerChoices = props => {
33386
33828
  }));
33387
33829
  };
33388
33830
 
33389
- const strings = {
33390
- hideAnswersToggleLabel: i18n._("Hide answer choices")
33391
- };
33392
-
33393
33831
  const HideAnswersToggle = props => {
33394
33832
  const idFactory = useUniqueIdWithMock();
33395
33833
  const switchId = idFactory.get("hide-answers-toggle");
33396
33834
  const labelId = idFactory.get("hide-answers-label");
33835
+ const {
33836
+ strings
33837
+ } = usePerseusI18n();
33397
33838
  return /*#__PURE__*/React.createElement(View, {
33398
33839
  style: styles$g.switchWrapper
33399
33840
  }, /*#__PURE__*/React.createElement(Switch, {
@@ -33434,9 +33875,12 @@ const AnswerPill = props => {
33434
33875
  hovered
33435
33876
  } = props;
33436
33877
  const idFactory = useUniqueIdWithMock();
33437
- const answerString = selectedAnswers.length > 1 ?
33438
- // always need `ngettext` for variable numbers even if we don't use the singular, see https://khanacademy.slack.com/archives/C0918TZ5G/p1700163024293079
33439
- i18n.ngettext("%(num)s answer", "%(num)s answers", selectedAnswers.length) : selectedAnswers[0];
33878
+ const {
33879
+ strings
33880
+ } = usePerseusI18n();
33881
+ const answerString = selectedAnswers.length > 1 ? strings.answers({
33882
+ num: selectedAnswers.length
33883
+ }) : selectedAnswers[0];
33440
33884
 
33441
33885
  // It should no longer be possible to interact with an answer after it
33442
33886
  // has been labeled (answered) correctly.
@@ -33463,6 +33907,7 @@ const AnswerPill = props => {
33463
33907
  style: [style, popperStyle, styles$f.pill, correct && styles$f.correct, incorrect && styles$f.incorrect, (focused || hovered) && BringToFront]
33464
33908
  }, /*#__PURE__*/React.createElement(Renderer, {
33465
33909
  content: answerString,
33910
+ strings: strings,
33466
33911
  inline: true
33467
33912
  })));
33468
33913
  };
@@ -33557,10 +34002,8 @@ class Marker extends React.Component {
33557
34002
  const markerDisabled = showCorrectness === "correct";
33558
34003
  const active = hovered || focused;
33559
34004
  return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(View, {
33560
- style: [styles$e.marker, active && !markerDisabled && styles$e.markerActive]
33561
- // already translated in: /widgets/graded-group.tsx
33562
- ,
33563
- "aria-label": markerDisabled ? i18n._("Correct!") : label
34005
+ style: [styles$e.marker, active && !markerDisabled && styles$e.markerActive],
34006
+ "aria-label": markerDisabled ? this.context.strings.correctExcited : label
33564
34007
  }, this.renderIcon()), !!selected && showAnswer && /*#__PURE__*/React.createElement(AnswerPill, {
33565
34008
  selectedAnswers: selected,
33566
34009
  showCorrectness: showCorrectness,
@@ -33572,6 +34015,7 @@ class Marker extends React.Component {
33572
34015
  }));
33573
34016
  }
33574
34017
  }
34018
+ Marker.contextType = PerseusI18nContext;
33575
34019
  Marker.defaultProps = {
33576
34020
  selected: []
33577
34021
  };
@@ -34143,8 +34587,11 @@ class LabelImage extends React.Component {
34143
34587
  multipleAnswers,
34144
34588
  hideChoicesFromInstructions: hideChoices
34145
34589
  } = this.props;
34146
- const promptString = isMobile ? multipleAnswers ? i18n._("Tap each dot on the image to select all answers that apply.") : i18n._("Tap each dot on the image to select an answer.") : multipleAnswers ? i18n._("Click each dot on the image to select all answers that apply.") : i18n._("Click each dot on the image to select an answer.");
34147
- const choicesString = i18n._("Choices:");
34590
+ const {
34591
+ strings
34592
+ } = this.context;
34593
+ const promptString = isMobile ? multipleAnswers ? strings.tapMultiple : strings.tapSingle : multipleAnswers ? strings.clickMultiple : strings.clickSingle;
34594
+ const choicesString = strings.choices;
34148
34595
  return /*#__PURE__*/React.createElement("div", {
34149
34596
  className: classNames("perseus-label-image-widget-instructions", css(styles$d.instructions))
34150
34597
  }, /*#__PURE__*/React.createElement("div", {
@@ -34155,7 +34602,8 @@ class LabelImage extends React.Component {
34155
34602
  className: css(styles$d.instructionsChoice),
34156
34603
  key: index
34157
34604
  }, /*#__PURE__*/React.createElement(Renderer, {
34158
- content: choice
34605
+ content: choice,
34606
+ strings: strings
34159
34607
  })))));
34160
34608
  }
34161
34609
  render() {
@@ -34206,6 +34654,7 @@ class LabelImage extends React.Component {
34206
34654
  }));
34207
34655
  }
34208
34656
  }
34657
+ LabelImage.contextType = PerseusI18nContext;
34209
34658
  const styles$d = StyleSheet.create({
34210
34659
  instructions: {
34211
34660
  paddingBottom: 16
@@ -34519,7 +34968,8 @@ class Draggable extends React.Component {
34519
34968
  }, /*#__PURE__*/React.createElement(Renderer, {
34520
34969
  content: this.props.content,
34521
34970
  linterContext: PerseusLinter.pushContextStack(this.props.linterContext, "draggable"),
34522
- onRender: this.props.onRender
34971
+ onRender: this.props.onRender,
34972
+ strings: this.context.strings
34523
34973
  }));
34524
34974
  }
34525
34975
  componentDidUpdate(prevProps) {
@@ -34544,6 +34994,7 @@ class Draggable extends React.Component {
34544
34994
  }
34545
34995
  }
34546
34996
  }
34997
+ Draggable.contextType = PerseusI18nContext;
34547
34998
  Draggable.defaultProps = {
34548
34999
  includePadding: true,
34549
35000
  type: ItemState.STATIC,
@@ -35104,12 +35555,14 @@ class Matcher extends React.Component {
35104
35555
  className: css(styles$b.column, styles$b.columnLabel)
35105
35556
  }, /*#__PURE__*/React.createElement(Renderer, {
35106
35557
  content: this.props.labels[0] || "...",
35107
- linterContext: this.props.linterContext
35558
+ linterContext: this.props.linterContext,
35559
+ strings: this.context.strings
35108
35560
  })), /*#__PURE__*/React.createElement("th", {
35109
35561
  className: css(styles$b.column, styles$b.columnRight, styles$b.columnLabel)
35110
35562
  }, /*#__PURE__*/React.createElement(Renderer, {
35111
35563
  content: this.props.labels[1] || "...",
35112
- linterContext: this.props.linterContext
35564
+ linterContext: this.props.linterContext,
35565
+ strings: this.context.strings
35113
35566
  }))), /*#__PURE__*/React.createElement("tr", {
35114
35567
  className: css(styles$b.row)
35115
35568
  }, /*#__PURE__*/React.createElement("td", {
@@ -35146,6 +35599,7 @@ class Matcher extends React.Component {
35146
35599
  })))));
35147
35600
  }
35148
35601
  }
35602
+ Matcher.contextType = PerseusI18nContext;
35149
35603
  Matcher.propTypes = {
35150
35604
  apiOptions: ApiOptions.propTypes,
35151
35605
  labels: PropTypes.array,
@@ -35431,8 +35885,58 @@ class Matrix extends React.Component {
35431
35885
  };
35432
35886
  };
35433
35887
  this.simpleValidate = rubric => {
35434
- // @ts-expect-error - TS2339 - Property 'validate' does not exist on type 'typeof Matrix'.
35435
- return Matrix.validate(this.getUserInput(), rubric);
35888
+ return Matrix.validate(this.getUserInput(), rubric, this.context.strings);
35889
+ };
35890
+ }
35891
+ static validate(state, rubric, strings) {
35892
+ const solution = rubric.answers;
35893
+ const supplied = state.answers;
35894
+ const solutionSize = getMatrixSize(solution);
35895
+ const suppliedSize = getMatrixSize(supplied);
35896
+ const incorrectSize = solutionSize[0] !== suppliedSize[0] || solutionSize[1] !== suppliedSize[1];
35897
+ const createValidator = KhanAnswerTypes.number.createValidatorFunctional;
35898
+ let message = null;
35899
+ let hasEmptyCell = false;
35900
+ let incorrect = false;
35901
+ _(suppliedSize[0]).times(row => {
35902
+ _(suppliedSize[1]).times(col => {
35903
+ if (supplied[row][col] == null || supplied[row][col].toString().length === 0) {
35904
+ hasEmptyCell = true;
35905
+ }
35906
+ if (!incorrectSize) {
35907
+ const validator = createValidator(solution[row][col], {
35908
+ simplify: true
35909
+ }, strings);
35910
+ const result = validator(supplied[row][col]);
35911
+ if (result.message) {
35912
+ // @ts-expect-error - TS2322 - Type 'string' is not assignable to type 'null'.
35913
+ message = result.message;
35914
+ }
35915
+ if (!result.correct) {
35916
+ incorrect = true;
35917
+ }
35918
+ }
35919
+ });
35920
+ });
35921
+ if (hasEmptyCell) {
35922
+ return {
35923
+ type: "invalid",
35924
+ message: strings.fillAllCells
35925
+ };
35926
+ }
35927
+ if (incorrectSize) {
35928
+ return {
35929
+ type: "points",
35930
+ earned: 0,
35931
+ total: 1,
35932
+ message: null
35933
+ };
35934
+ }
35935
+ return {
35936
+ type: "points",
35937
+ earned: incorrect ? 0 : 1,
35938
+ total: 1,
35939
+ message: message
35436
35940
  };
35437
35941
  }
35438
35942
  componentDidMount() {
@@ -35474,7 +35978,8 @@ class Matrix extends React.Component {
35474
35978
  className: "matrix-prefix"
35475
35979
  }, /*#__PURE__*/React.createElement(Renderer, {
35476
35980
  content: this.props.prefix,
35477
- linterContext: this.props.linterContext
35981
+ linterContext: this.props.linterContext,
35982
+ strings: this.context.strings
35478
35983
  })), /*#__PURE__*/React.createElement("div", {
35479
35984
  className: "matrix-input"
35480
35985
  }, /*#__PURE__*/React.createElement("div", {
@@ -35587,10 +36092,12 @@ class Matrix extends React.Component {
35587
36092
  className: "matrix-suffix"
35588
36093
  }, /*#__PURE__*/React.createElement(Renderer, {
35589
36094
  content: this.props.suffix,
35590
- linterContext: this.props.linterContext
36095
+ linterContext: this.props.linterContext,
36096
+ strings: this.context.strings
35591
36097
  })));
35592
36098
  }
35593
36099
  }
36100
+ Matrix.contextType = PerseusI18nContext;
35594
36101
  Matrix.defaultProps = {
35595
36102
  matrixBoardSize: [3, 3],
35596
36103
  answers: [[]],
@@ -35600,59 +36107,6 @@ Matrix.defaultProps = {
35600
36107
  apiOptions: ApiOptions.defaults,
35601
36108
  linterContext: linterContextDefault
35602
36109
  };
35603
- _.extend(Matrix, {
35604
- validate: function (state, rubric) {
35605
- const solution = rubric.answers;
35606
- const supplied = state.answers;
35607
- const solutionSize = getMatrixSize(solution);
35608
- const suppliedSize = getMatrixSize(supplied);
35609
- const incorrectSize = solutionSize[0] !== suppliedSize[0] || solutionSize[1] !== suppliedSize[1];
35610
- const createValidator = KhanAnswerTypes.number.createValidatorFunctional;
35611
- let message = null;
35612
- let hasEmptyCell = false;
35613
- let incorrect = false;
35614
- _(suppliedSize[0]).times(row => {
35615
- _(suppliedSize[1]).times(col => {
35616
- if (supplied[row][col] == null || supplied[row][col].toString().length === 0) {
35617
- hasEmptyCell = true;
35618
- }
35619
- if (!incorrectSize) {
35620
- const validator = createValidator(solution[row][col], {
35621
- simplify: true
35622
- });
35623
- const result = validator(supplied[row][col]);
35624
- if (result.message) {
35625
- // @ts-expect-error - TS2322 - Type 'string' is not assignable to type 'null'.
35626
- message = result.message;
35627
- }
35628
- if (!result.correct) {
35629
- incorrect = true;
35630
- }
35631
- }
35632
- });
35633
- });
35634
- if (hasEmptyCell) {
35635
- return {
35636
- type: "invalid",
35637
- message: i18n._("Make sure you fill in all cells in the matrix.")
35638
- };
35639
- }
35640
- if (incorrectSize) {
35641
- return {
35642
- type: "points",
35643
- earned: 0,
35644
- total: 1,
35645
- message: null
35646
- };
35647
- }
35648
- return {
35649
- type: "points",
35650
- earned: incorrect ? 0 : 1,
35651
- total: 1,
35652
- message: message
35653
- };
35654
- }
35655
- });
35656
36110
  const propTransform$1 = editorProps => {
35657
36111
  // Remove answers before passing to widget
35658
36112
  const blankAnswers = _(editorProps.matrixBoardSize[0]).times(function () {
@@ -36690,8 +37144,8 @@ class Molecule extends React.Component {
36690
37144
  // eslint-disable-next-line react/no-string-refs
36691
37145
  ,
36692
37146
  ref: "canvas"
36693
- }, i18n.$_("A molecular structure drawing. SMILES notation: %(content)s", {
36694
- content: this.props.smiles
37147
+ }, this.context.strings.molecularDrawing({
37148
+ content: this.props.smiles || ""
36695
37149
  }));
36696
37150
  if (this.state.error) {
36697
37151
  content = /*#__PURE__*/React.createElement("div", {
@@ -36703,6 +37157,7 @@ class Molecule extends React.Component {
36703
37157
  }, content);
36704
37158
  }
36705
37159
  }
37160
+ Molecule.contextType = PerseusI18nContext;
36706
37161
  class MoleculeWidget extends React.Component {
36707
37162
  constructor(...args) {
36708
37163
  super(...args);
@@ -37165,18 +37620,21 @@ class NumberLine extends React.Component {
37165
37620
  };
37166
37621
  }
37167
37622
  render() {
37623
+ const {
37624
+ strings
37625
+ } = this.context;
37168
37626
  const divisionRange = this.props.divisionRange;
37169
37627
  const divRangeString = divisionRange[0] + EN_DASH + divisionRange[1];
37170
37628
  const invalidNumDivisions = this.props.numDivisions < divisionRange[0] || this.props.numDivisions > divisionRange[1];
37171
37629
  const inequalityControls = /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement("input", {
37172
37630
  type: "button",
37173
37631
  className: "simple-button",
37174
- value: i18n._("Switch direction"),
37632
+ value: strings.switchDirection,
37175
37633
  onClick: this.handleReverse
37176
37634
  }), /*#__PURE__*/React.createElement("input", {
37177
37635
  type: "button",
37178
37636
  className: "simple-button",
37179
- value: _(["le", "ge"]).contains(this.props.rel) ? i18n._("Make circle open") : i18n._("Make circle filled"),
37637
+ value: _(["le", "ge"]).contains(this.props.rel) ? strings.circleOpen : strings.circleFilled,
37180
37638
  onClick: this.handleToggleStrict
37181
37639
  }));
37182
37640
  let tickCtrl;
@@ -37187,7 +37645,7 @@ class NumberLine extends React.Component {
37187
37645
  } else {
37188
37646
  Input = NumberInput;
37189
37647
  }
37190
- tickCtrl = /*#__PURE__*/React.createElement("label", null, i18n._("Number of divisions:"), " ", /*#__PURE__*/React.createElement(Input
37648
+ tickCtrl = /*#__PURE__*/React.createElement("label", null, strings.numDivisions, " ", /*#__PURE__*/React.createElement(Input
37191
37649
  // eslint-disable-next-line react/no-string-refs
37192
37650
  , {
37193
37651
  ref: "tick-ctrl",
@@ -37204,13 +37662,14 @@ class NumberLine extends React.Component {
37204
37662
  className: "perseus-widget " + "perseus-widget-interactive-number-line"
37205
37663
  }, tickCtrl, !this.isValid() ? /*#__PURE__*/React.createElement("div", {
37206
37664
  className: "perseus-error"
37207
- }, i18n.doNotTranslate("Invalid number line configuration.")) : this.props.isTickCtrl && invalidNumDivisions ? /*#__PURE__*/React.createElement("div", {
37665
+ }, "Invalid number line configuration.") : this.props.isTickCtrl && invalidNumDivisions ? /*#__PURE__*/React.createElement("div", {
37208
37666
  className: "perseus-error"
37209
- }, i18n.$_("Please make sure the number of divisions is " + "in the range %(divRangeString)s.", {
37667
+ }, strings.divisions({
37210
37668
  divRangeString: divRangeString
37211
37669
  })) : this._renderGraphie(), !this.props.static && this.props.isInequality && inequalityControls);
37212
37670
  }
37213
37671
  }
37672
+ NumberLine.contextType = PerseusI18nContext;
37214
37673
  NumberLine.defaultProps = {
37215
37674
  range: [0, 10],
37216
37675
  labelStyle: "decimal",
@@ -37437,7 +37896,8 @@ class Card extends React.Component {
37437
37896
  }, /*#__PURE__*/React.createElement("div", {
37438
37897
  className: className.join(" ")
37439
37898
  }, /*#__PURE__*/React.createElement(Renderer, _extends({}, rendererProps, {
37440
- linterContext: this.props.linterContext
37899
+ linterContext: this.props.linterContext,
37900
+ strings: this.context.strings
37441
37901
  }))));
37442
37902
  }
37443
37903
  shouldComponentUpdate(nextProps, nextState) {
@@ -37490,6 +37950,7 @@ class Card extends React.Component {
37490
37950
  document.removeEventListener("touchmove", this.onMouseMove);
37491
37951
  }
37492
37952
  }
37953
+ Card.contextType = PerseusI18nContext;
37493
37954
  Card.propTypes = {
37494
37955
  floating: PropTypes.bool.isRequired,
37495
37956
  animating: PropTypes.bool,
@@ -38779,7 +39240,7 @@ class HighlightSetRenderer extends React.PureComponent {
38779
39240
  }
38780
39241
  const hoveredHighlight = this.props.highlights[hoveredHighlightKey];
38781
39242
  return /*#__PURE__*/React.createElement(HighlightTooltip, {
38782
- label: i18n._("Remove highlight"),
39243
+ label: this.context.strings.removeHighlight,
38783
39244
  focusNode: hoveredHighlight.domRange.endContainer,
38784
39245
  focusOffset: hoveredHighlight.domRange.endOffset,
38785
39246
  offsetParent: this.props.offsetParent,
@@ -38809,6 +39270,7 @@ class HighlightSetRenderer extends React.PureComponent {
38809
39270
  })), this.props.editable && this._renderTooltip());
38810
39271
  }
38811
39272
  }
39273
+ HighlightSetRenderer.contextType = PerseusI18nContext;
38812
39274
 
38813
39275
  /* eslint-disable react/no-unsafe */
38814
39276
  class SelectionTracker extends React.PureComponent {
@@ -39009,7 +39471,7 @@ class HighlightingUI extends React.PureComponent {
39009
39471
  onRemoveHighlight: this.props.onRemoveHighlight,
39010
39472
  zIndexes: this.props.zIndexes
39011
39473
  }), this._selectionIsValid(trackedSelection) && !userIsMouseSelecting && /*#__PURE__*/React.createElement(HighlightTooltip, {
39012
- label: i18n._("Add highlight"),
39474
+ label: this.context.strings.addHighlight,
39013
39475
  onClick: () => this._handleAddHighlight(trackedSelection.proposedHighlight),
39014
39476
  focusNode: trackedSelection.focusNode,
39015
39477
  focusOffset: trackedSelection.focusOffset,
@@ -39017,6 +39479,7 @@ class HighlightingUI extends React.PureComponent {
39017
39479
  })));
39018
39480
  }
39019
39481
  }
39482
+ HighlightingUI.contextType = PerseusI18nContext;
39020
39483
 
39021
39484
  /**
39022
39485
  * This component, whenever its content updates, traverses the DOM to find
@@ -39307,16 +39770,30 @@ class RefStart extends React.Component {
39307
39770
  render() {
39308
39771
  return /*#__PURE__*/React.createElement("span", {
39309
39772
  style: REF_STYLE
39310
- }, i18n.doNotTranslate("_"));
39773
+ }, "_");
39311
39774
  }
39312
39775
  }
39313
39776
  class RefEnd extends React.Component {
39314
39777
  render() {
39315
39778
  return /*#__PURE__*/React.createElement("span", {
39316
39779
  style: REF_STYLE
39317
- }, i18n.doNotTranslate("_"));
39780
+ }, "_");
39318
39781
  }
39319
39782
  }
39783
+ const AltText = ({
39784
+ id,
39785
+ number
39786
+ }) => {
39787
+ const {
39788
+ strings
39789
+ } = usePerseusI18n();
39790
+ return /*#__PURE__*/React.createElement("span", {
39791
+ key: "alt-text",
39792
+ className: "perseus-sr-only"
39793
+ }, strings[id]({
39794
+ number
39795
+ }));
39796
+ };
39320
39797
  const rules = {
39321
39798
  newline: SimpleMarkdown.defaultRules.newline,
39322
39799
  paragraph: SimpleMarkdown.defaultRules.paragraph,
@@ -39485,12 +39962,10 @@ const rules = {
39485
39962
  "aria-hidden": "true"
39486
39963
  }, /*#__PURE__*/React.createElement("span", {
39487
39964
  style: SQUARE_LABEL_STYLE
39488
- }, node.content)), /*#__PURE__*/React.createElement("span", {
39489
- key: "alt-text",
39490
- className: "perseus-sr-only"
39491
- }, i18n.$_("[Marker for question %(number)s]", {
39965
+ }, node.content)), /*#__PURE__*/React.createElement(AltText, {
39966
+ id: "questionMarker",
39492
39967
  number: node.content
39493
- })), node.space ? "\u00A0" : null];
39968
+ }), node.space ? "\u00A0" : null];
39494
39969
  }
39495
39970
  },
39496
39971
  circleLabel: {
@@ -39511,12 +39986,10 @@ const rules = {
39511
39986
  "aria-hidden": true
39512
39987
  }, /*#__PURE__*/React.createElement("span", {
39513
39988
  style: CIRCLE_LABEL_STYLE
39514
- }, node.content)), /*#__PURE__*/React.createElement("span", {
39515
- key: "alt-text",
39516
- className: "perseus-sr-only"
39517
- }, i18n.$_("[Circle marker %(number)s]", {
39989
+ }, node.content)), /*#__PURE__*/React.createElement(AltText, {
39990
+ id: "circleMarker",
39518
39991
  number: node.content
39519
- })), node.space ? "\u00A0" : null];
39992
+ }), node.space ? "\u00A0" : null];
39520
39993
  }
39521
39994
  },
39522
39995
  squareBracketRef: {
@@ -39536,12 +40009,10 @@ const rules = {
39536
40009
  key: "visual-brackets",
39537
40010
  className: "perseus-passage-bracket-label",
39538
40011
  "aria-hidden": "true"
39539
- }, "[", node.content, "]"), /*#__PURE__*/React.createElement("span", {
39540
- key: "alt-text",
39541
- className: "perseus-sr-only"
39542
- }, i18n.$_("[Sentence %(number)s]", {
40012
+ }, "[", node.content, "]"), /*#__PURE__*/React.createElement(AltText, {
40013
+ id: "sentenceMarker",
39543
40014
  number: node.content
39544
- })), node.space ? "\u00A0" : null];
40015
+ }), node.space ? "\u00A0" : null];
39545
40016
  }
39546
40017
  },
39547
40018
  highlight: {
@@ -39932,15 +40403,18 @@ class Passage extends React.Component {
39932
40403
  _renderInstructions(parseState) {
39933
40404
  const firstQuestionNumber = parseState.firstQuestionRef;
39934
40405
  const firstSentenceRef = parseState.firstSentenceRef;
40406
+ const {
40407
+ strings
40408
+ } = this.context;
39935
40409
  let instructions = "";
39936
40410
  if (firstQuestionNumber) {
39937
- instructions += i18n._("The symbol %(questionSymbol)s indicates that question " + "%(questionNumber)s references this portion of the " + "passage.", {
40411
+ instructions += strings.symbolPassage({
39938
40412
  questionSymbol: "[[" + firstQuestionNumber + "]]",
39939
40413
  questionNumber: firstQuestionNumber
39940
40414
  });
39941
40415
  }
39942
40416
  if (firstSentenceRef) {
39943
- instructions += i18n._(" The symbol %(sentenceSymbol)s indicates that the " + "following sentence is referenced in a question.", {
40417
+ instructions += strings.symbolQuestion({
39944
40418
  sentenceSymbol: "[" + firstSentenceRef + "]"
39945
40419
  });
39946
40420
  }
@@ -39986,6 +40460,9 @@ class Passage extends React.Component {
39986
40460
  return PassageMarkdown.output(parsed);
39987
40461
  }
39988
40462
  render() {
40463
+ const {
40464
+ strings
40465
+ } = this.context;
39989
40466
  let lineNumbers;
39990
40467
  const nLines = this.state.nLines;
39991
40468
  if (this.props.showLineNumbers && nLines) {
@@ -39994,9 +40471,7 @@ class Passage extends React.Component {
39994
40471
  lineNumbers = _.range(1, nLines + 1).map(lineN => {
39995
40472
  const lineAt = lineN + this.state.startLineNumbersAfter;
39996
40473
  if (lineAt === 4) {
39997
- // we leave a space in i18n to disambiguate translations, see TP-5392
39998
- // I18N: a label next to a reading passage to denote the line number
39999
- const translatedLine = i18n._("Line ");
40474
+ const translatedLine = strings.lineLabel;
40000
40475
  return /*#__PURE__*/React.createElement("span", {
40001
40476
  key: "line-marker",
40002
40477
  className: "line-marker"
@@ -40026,13 +40501,14 @@ class Passage extends React.Component {
40026
40501
  className: "passage-title"
40027
40502
  }, /*#__PURE__*/React.createElement(Renderer, {
40028
40503
  content: this.props.passageTitle,
40029
- linterContext: this.props.linterContext
40504
+ linterContext: this.props.linterContext,
40505
+ strings: strings
40030
40506
  })), lineNumbers && /*#__PURE__*/React.createElement("div", {
40031
40507
  className: "line-numbers",
40032
40508
  "aria-hidden": true
40033
40509
  }, lineNumbers), !hasTitle && /*#__PURE__*/React.createElement("h3", {
40034
40510
  className: "perseus-sr-only"
40035
- }, i18n._("Beginning of reading passage.")), /*#__PURE__*/React.createElement("div", {
40511
+ }, strings.beginningPassage), /*#__PURE__*/React.createElement("div", {
40036
40512
  className: "passage-text"
40037
40513
  }, this.shouldRenderJipt() ?
40038
40514
  /*#__PURE__*/
@@ -40040,18 +40516,20 @@ class Passage extends React.Component {
40040
40516
  // content to a <Renderer /> which knows how
40041
40517
  // to handle rendering JIPT text.
40042
40518
  React.createElement(Renderer, {
40043
- content: this.props.passageText
40519
+ content: this.props.passageText,
40520
+ strings: strings
40044
40521
  }) : this._renderContent(parsedContent)), this._hasFootnotes() && [/*#__PURE__*/React.createElement("h4", {
40045
40522
  key: "footnote-start",
40046
40523
  className: "perseus-sr-only"
40047
- }, i18n._("Beginning of reading passage footnotes.")), /*#__PURE__*/React.createElement("div", {
40524
+ }, strings.beginningFootnotes), /*#__PURE__*/React.createElement("div", {
40048
40525
  key: "footnotes",
40049
40526
  className: "footnotes"
40050
40527
  }, this._renderFootnotes())], /*#__PURE__*/React.createElement("div", {
40051
40528
  className: "perseus-sr-only"
40052
- }, i18n._("End of reading passage.")))));
40529
+ }, strings.endPassage))));
40053
40530
  }
40054
40531
  }
40532
+ Passage.contextType = PerseusI18nContext;
40055
40533
  Passage.defaultProps = {
40056
40534
  passageTitle: "",
40057
40535
  passageText: "",
@@ -40102,10 +40580,12 @@ class PassageRefTarget extends React.Component {
40102
40580
  // inline={true}
40103
40581
  ,
40104
40582
  apiOptions: this.props.apiOptions,
40105
- linterContext: this.props.linterContext
40583
+ linterContext: this.props.linterContext,
40584
+ strings: this.context.strings
40106
40585
  });
40107
40586
  }
40108
40587
  }
40588
+ PassageRefTarget.contextType = PerseusI18nContext;
40109
40589
  PassageRefTarget.defaultProps = {
40110
40590
  content: "",
40111
40591
  linterContext: linterContextDefault
@@ -40289,11 +40769,11 @@ class Plotter extends React.Component {
40289
40769
  }
40290
40770
  }
40291
40771
  if ((isBar || isLine) && isMobile && !this.props.static) {
40292
- self.graphie.dragPrompt = graphie.label([c.dimX / 2, c.dimY / 2], i18n._("Drag handles to make graph"), "center", false).css("font-weight", "bold").css("color", KhanColors.KA_GREEN).css("display", "none");
40772
+ self.graphie.dragPrompt = graphie.label([c.dimX / 2, c.dimY / 2], this.context.strings.dragHandles, "center", false).css("font-weight", "bold").css("color", KhanColors.KA_GREEN).css("display", "none");
40293
40773
  }
40294
40774
  self.setupCategories(config);
40295
40775
  if (isTiledPlot && isMobile) {
40296
- self.graphie.dotPrompt = graphie.label([c.dimX / 2, c.dimY / 2], i18n._("Tap to add points"), "center", false).css("font-weight", "bold").css("color", KhanColors.KA_GREEN).css("display", "none");
40776
+ self.graphie.dotPrompt = graphie.label([c.dimX / 2, c.dimY / 2], this.context.strings.tapAddPoints, "center", false).css("font-weight", "bold").css("color", KhanColors.KA_GREEN).css("display", "none");
40297
40777
  }
40298
40778
  if (isTiledPlot) {
40299
40779
  self.drawPicHeights(self.state.values, prevState.values);
@@ -40908,6 +41388,7 @@ class Plotter extends React.Component {
40908
41388
  });
40909
41389
  }
40910
41390
  }
41391
+ Plotter.contextType = PerseusI18nContext;
40911
41392
  Plotter.defaultProps = {
40912
41393
  type: "bar",
40913
41394
  labels: ["", ""],
@@ -41179,7 +41660,7 @@ class Table extends React.Component {
41179
41660
  };
41180
41661
  this.simpleValidate = rubric => {
41181
41662
  // @ts-expect-error - TS2339 - Property 'validate' does not exist on type 'typeof Table'.
41182
- return Table.validate(this.getUserInput(), rubric);
41663
+ return Table.validate(this.getUserInput(), rubric, this.context.strings);
41183
41664
  };
41184
41665
  this._handleFocus = inputPath => {
41185
41666
  this.props.onFocus(inputPath);
@@ -41284,7 +41765,8 @@ class Table extends React.Component {
41284
41765
  key: i
41285
41766
  }, /*#__PURE__*/React.createElement(Renderer, {
41286
41767
  content: header,
41287
- linterContext: this.props.linterContext
41768
+ linterContext: this.props.linterContext,
41769
+ strings: this.context.strings
41288
41770
  }));
41289
41771
  }))), /*#__PURE__*/React.createElement("tbody", null, _(rows).times(r => {
41290
41772
  return /*#__PURE__*/React.createElement("tr", {
@@ -41306,6 +41788,7 @@ class Table extends React.Component {
41306
41788
  })));
41307
41789
  }
41308
41790
  }
41791
+ Table.contextType = PerseusI18nContext;
41309
41792
  Table.propTypes = {
41310
41793
  answers: PropTypes.arrayOf(PropTypes.arrayOf(PropTypes.string)),
41311
41794
  editableHeaders: PropTypes.bool,
@@ -41333,7 +41816,7 @@ Table.defaultProps = function () {
41333
41816
  };
41334
41817
  }();
41335
41818
  _.extend(Table, {
41336
- validate: function (state, rubric) {
41819
+ validate: function (state, rubric, strings) {
41337
41820
  const filterNonEmpty = function filterNonEmpty(table) {
41338
41821
  return _.filter(table, function (row) {
41339
41822
  // Check if row has a cell that is nonempty
@@ -41371,7 +41854,7 @@ _.extend(Table, {
41371
41854
  const cellSolution = rowSolution[i];
41372
41855
  const validator = createValidator(cellSolution, {
41373
41856
  simplify: true
41374
- });
41857
+ }, strings);
41375
41858
  const result = validator(cellSupplied);
41376
41859
  if (result.message) {
41377
41860
  // @ts-expect-error - TS2322 - Type 'string' is not assignable to type 'null'.
@@ -42134,7 +42617,7 @@ class OldUnitInput extends React.Component {
42134
42617
  });
42135
42618
  };
42136
42619
  this.simpleValidate = (rubric, onInputError) => {
42137
- return OldUnitInput.validate(this.getUserInput(), rubric);
42620
+ return OldUnitInput.validate(this.getUserInput(), rubric, this.context.strings);
42138
42621
  };
42139
42622
  this.getUserInput = () => {
42140
42623
  return this.props.value;
@@ -42187,13 +42670,13 @@ class OldUnitInput extends React.Component {
42187
42670
  return "unit";
42188
42671
  };
42189
42672
  }
42190
- static validate(userInput, rubric) {
42673
+ static validate(userInput, rubric, strings) {
42191
42674
  const answer = KAS.unitParse(rubric.value).expr;
42192
42675
  const guess = KAS.unitParse(userInput);
42193
42676
  if (!guess.parsed) {
42194
42677
  return {
42195
42678
  type: "invalid",
42196
- message: i18n._("I couldn't understand those units.")
42679
+ message: strings.dontUnderstandUnits
42197
42680
  };
42198
42681
  }
42199
42682
 
@@ -42208,8 +42691,7 @@ class OldUnitInput extends React.Component {
42208
42691
  const sigfigs = rubric.sigfigs;
42209
42692
  const sigfigsCorrect = countSigfigs(guess.coefficient) === sigfigs;
42210
42693
  if (!sigfigsCorrect) {
42211
- // @ts-expect-error - TS2322 - Type 'string' is not assignable to type 'null'.
42212
- message = i18n._("Check your significant figures.");
42694
+ message = strings.checkSigFigs;
42213
42695
  }
42214
42696
 
42215
42697
  // now we need to check that the answer is correct to the precision we
@@ -42228,8 +42710,7 @@ class OldUnitInput extends React.Component {
42228
42710
  numericallyCorrect = false;
42229
42711
  }
42230
42712
  if (!numericallyCorrect) {
42231
- // @ts-expect-error - TS2322 - Type 'string' is not assignable to type 'null'.
42232
- message = i18n._("That answer is numerically incorrect.");
42713
+ message = strings.answerNumericallyIncorrect;
42233
42714
  }
42234
42715
  let kasCorrect;
42235
42716
  const guessUnit = primUnits(guess.expr.simplify());
@@ -42249,8 +42730,7 @@ class OldUnitInput extends React.Component {
42249
42730
  });
42250
42731
  }
42251
42732
  if (!kasCorrect) {
42252
- // @ts-expect-error - TS2322 - Type 'string' is not assignable to type 'null'.
42253
- message = i18n._("Check your units.");
42733
+ message = strings.checkUnits;
42254
42734
  }
42255
42735
  const correct = kasCorrect && numericallyCorrect && sigfigsCorrect;
42256
42736
  return {
@@ -42299,12 +42779,13 @@ class OldUnitInput extends React.Component {
42299
42779
  style: {
42300
42780
  display: "none"
42301
42781
  }
42302
- }, i18n._("I don't understand that")));
42782
+ }, this.context.strings.dontUnderstand));
42303
42783
  }
42304
42784
  }
42305
42785
 
42306
42786
  // Extract the primitive units from a unit expression. This first simplifies
42307
42787
  // `expr` to a `Mul` like "5 kg m / s^2" then removes the first term.
42788
+ OldUnitInput.contextType = PerseusI18nContext;
42308
42789
  OldUnitInput.defaultProps = {
42309
42790
  apiOptions: ApiOptions.defaults,
42310
42791
  value: ""
@@ -42355,9 +42836,12 @@ const VideoTranscriptLink = props => {
42355
42836
  // which is used by our GraphQL framework.
42356
42837
  // @ts-expect-error - TS2345 - Argument of type 'string' is not assignable to parameter of type 'VideoKind'.
42357
42838
  const result = useVideo(id, kind);
42839
+ const {
42840
+ strings
42841
+ } = usePerseusI18n();
42358
42842
  switch (result.status) {
42359
42843
  case "loading":
42360
- return /*#__PURE__*/React.createElement(View, null, i18n._("Loading..."));
42844
+ return /*#__PURE__*/React.createElement(View, null, strings.loading);
42361
42845
  case "success":
42362
42846
  {
42363
42847
  var _result$data;
@@ -42370,14 +42854,14 @@ const VideoTranscriptLink = props => {
42370
42854
  href: "/transcript/" + ((video == null ? void 0 : video.contentId) || "videoNotFound"),
42371
42855
  target: "_blank",
42372
42856
  className: "visited-no-recolor"
42373
- }, i18n._("See video transcript")));
42857
+ }, strings.videoTranscript));
42374
42858
  }
42375
42859
  case "error":
42376
- return /*#__PURE__*/React.createElement(View, null, i18n._("Something went wrong."));
42860
+ return /*#__PURE__*/React.createElement(View, null, strings.somethingWrong);
42377
42861
  case "aborted":
42378
- return /*#__PURE__*/React.createElement(View, null, i18n._("Something went wrong."));
42862
+ return /*#__PURE__*/React.createElement(View, null, strings.somethingWrong);
42379
42863
  default:
42380
- return /*#__PURE__*/React.createElement(View, null, i18n._("Something went wrong."));
42864
+ return /*#__PURE__*/React.createElement(View, null, strings.somethingWrong);
42381
42865
  }
42382
42866
  };
42383
42867
  const styles$4 = StyleSheet.create({
@@ -42469,7 +42953,7 @@ class Video extends React.Component {
42469
42953
  key: location + this.props.alignment
42470
42954
  }, /*#__PURE__*/React.createElement(View, {
42471
42955
  style: a11y.srOnly
42472
- }, i18n._("Khan Academy video wrapper")), /*#__PURE__*/React.createElement("iframe", {
42956
+ }, this.context.strings.videoWrapper), /*#__PURE__*/React.createElement("iframe", {
42473
42957
  // TODO(joshuan): Consider not using iframes when we're
42474
42958
  // loading this from webapp. This iframe is problematic
42475
42959
  // for screenreaders.
@@ -42484,6 +42968,7 @@ class Video extends React.Component {
42484
42968
  }));
42485
42969
  }
42486
42970
  }
42971
+ Video.contextType = PerseusI18nContext;
42487
42972
  var Video$1 = {
42488
42973
  name: "video",
42489
42974
  displayName: "Video",
@@ -42533,7 +43018,7 @@ const init = function init(options) {
42533
43018
 
42534
43019
  // This file is processed by a Rollup plugin (replace) to inject the production
42535
43020
  const libName = "@khanacademy/perseus";
42536
- const libVersion = "21.6.0";
43021
+ const libVersion = "22.0.1";
42537
43022
  addLibraryVersionToPerseusDebug(libName, libVersion);
42538
43023
 
42539
43024
  const apiVersion = {
@@ -42699,8 +43184,9 @@ class ArticleRenderer extends React.Component {
42699
43184
  }
42700
43185
  }),
42701
43186
  linterContext: PerseusLinter.pushContextStack(this.props.linterContext, "article"),
42702
- legacyPerseusLint: this.props.legacyPerseusLint
42703
- }, this.props.dependencies)));
43187
+ legacyPerseusLint: this.props.legacyPerseusLint,
43188
+ strings: this.context.strings
43189
+ })));
42704
43190
  });
42705
43191
  return /*#__PURE__*/React.createElement("div", {
42706
43192
  className: classes
@@ -42709,6 +43195,7 @@ class ArticleRenderer extends React.Component {
42709
43195
  }, sections));
42710
43196
  }
42711
43197
  }
43198
+ ArticleRenderer.contextType = PerseusI18nContext;
42712
43199
  ArticleRenderer.defaultProps = {
42713
43200
  apiOptions: ApiOptions.defaults,
42714
43201
  useNewStyles: false,
@@ -42761,7 +43248,7 @@ class HintRenderer extends React.Component {
42761
43248
  tabIndex: "-1"
42762
43249
  }, !apiOptions.isMobile && /*#__PURE__*/React.createElement("span", {
42763
43250
  className: "perseus-sr-only"
42764
- }, i18n._("Hint #%(pos)s", {
43251
+ }, this.context.strings.hintPos({
42765
43252
  pos: pos + 1
42766
43253
  })), !apiOptions.isMobile && totalHints != null && pos != null && /*#__PURE__*/React.createElement("span", {
42767
43254
  className: "perseus-hint-label",
@@ -42778,11 +43265,13 @@ class HintRenderer extends React.Component {
42778
43265
  images: hint.images,
42779
43266
  apiOptions: rendererApiOptions,
42780
43267
  findExternalWidgets: this.props.findExternalWidgets,
42781
- linterContext: PerseusLinter.pushContextStack(this.props.linterContext, "hint")
43268
+ linterContext: PerseusLinter.pushContextStack(this.props.linterContext, "hint"),
43269
+ strings: this.context.strings
42782
43270
  }))
42783
43271
  );
42784
43272
  }
42785
43273
  }
43274
+ HintRenderer.contextType = PerseusI18nContext;
42786
43275
  HintRenderer.defaultProps = {
42787
43276
  linterContext: PerseusLinter.linterContextDefault
42788
43277
  };
@@ -42964,7 +43453,7 @@ class HintsRenderer extends React.Component {
42964
43453
  className: classNames$1
42965
43454
  }, apiOptions.isMobile && hintsVisible > 0 && /*#__PURE__*/React.createElement("div", {
42966
43455
  className: css(styles$2.mobileHintStylesHintTitle, sharedStyles.responsiveLabel)
42967
- }, i18n._("Hints")), hints, showGetAnotherHint && /*#__PURE__*/React.createElement("button", {
43456
+ }, this.context.strings.hints), hints, showGetAnotherHint && /*#__PURE__*/React.createElement("button", {
42968
43457
  // @ts-expect-error - TS2322 - Type '{ children: Element[]; rel: string; className: string; onClick: (evt: MouseEvent<HTMLButtonElement, MouseEvent>) => void; }' is not assignable to type 'DetailedHTMLProps<ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>'.
42969
43458
  rel: "button",
42970
43459
  className: css(styles$2.linkButton, styles$2.getAnotherHintButton, apiOptions.isMobile && styles$2.mobileHintStylesGetAnotherHintButton),
@@ -42979,9 +43468,10 @@ class HintsRenderer extends React.Component {
42979
43468
  className: css(styles$2.plusText, apiOptions.isMobile && styles$2.mobileHintStylesPlusText)
42980
43469
  }, "+"), /*#__PURE__*/React.createElement("span", {
42981
43470
  className: css(styles$2.getAnotherHintText)
42982
- }, i18n._("Get another hint"), " ", hintRatioCopy)));
43471
+ }, this.context.strings.getAnotherHint, " ", hintRatioCopy)));
42983
43472
  }
42984
43473
  }
43474
+ HintsRenderer.contextType = PerseusI18nContext;
42985
43475
  HintsRenderer.defaultProps = {
42986
43476
  linterContext: PerseusLinter.linterContextDefault
42987
43477
  };
@@ -43372,14 +43862,16 @@ class ServerItemRenderer extends React.Component {
43372
43862
  content: this.props.item.question.content,
43373
43863
  widgets: this.props.item.question.widgets,
43374
43864
  images: this.props.item.question.images,
43375
- linterContext: PerseusLinter.pushContextStack(this.props.linterContext, "question")
43865
+ linterContext: PerseusLinter.pushContextStack(this.props.linterContext, "question"),
43866
+ strings: this.context.strings
43376
43867
  }, this.props.dependencies)));
43377
43868
  const hintsRenderer = /*#__PURE__*/React.createElement(HintsRenderer, {
43378
43869
  hints: this.props.item.hints,
43379
43870
  hintsVisible: this.props.hintsVisible,
43380
43871
  apiOptions: apiOptions,
43381
43872
  ref: elem => this.hintsRenderer = elem,
43382
- linterContext: PerseusLinter.pushContextStack(this.props.linterContext, "hints")
43873
+ linterContext: PerseusLinter.pushContextStack(this.props.linterContext, "hints"),
43874
+ strings: this.context.strings
43383
43875
  });
43384
43876
  return /*#__PURE__*/React.createElement(DependenciesContext.Provider, {
43385
43877
  value: this.props.dependencies
@@ -43393,6 +43885,7 @@ class ServerItemRenderer extends React.Component {
43393
43885
  }, hintsRenderer)));
43394
43886
  }
43395
43887
  }
43888
+ ServerItemRenderer.contextType = PerseusI18nContext;
43396
43889
  ServerItemRenderer.defaultProps = {
43397
43890
  apiOptions: {},
43398
43891
  // a deep default is done in `this.update()`
@@ -43555,115 +44048,6 @@ var constants = /*#__PURE__*/Object.freeze({
43555
44048
  perseusFrameBorderWidth: perseusFrameBorderWidth
43556
44049
  });
43557
44050
 
43558
- // TODO(FEI-4010): Remove `Perseus` prefix for all types here
43559
- // TODO(FEI-4011): Use types generated by https://github.com/jaredly/generate-perseus-flowtypes
43560
-
43561
- // Range is replaced within this file with Interval, but it is used elsewhere
43562
- // and exported from the package, so we need to keep it around.
43563
- // TODO(FEI-5054): Figure out how to get global .d.ts files working with monorepos
43564
- const ItemExtras = [
43565
- // The user might benefit from using a Scientific Calculator. Provided on Khan Academy when true
43566
- "calculator",
43567
- // The user might benefit from using a statistics Chi Squared Table like https://people.richland.edu/james/lecture/m170/tbl-chi.html
43568
- "chi2Table",
43569
- // The user might benefit from a monthly payments calculator. Provided on Khan Academy when true
43570
- "financialCalculatorMonthlyPayment",
43571
- // The user might benefit from a total amount calculator. Provided on Khan Academy when true
43572
- "financialCalculatorTotalAmount",
43573
- // The user might benefit from a time to pay off calculator. Provided on Khan Academy when true
43574
- "financialCalculatorTimeToPayOff",
43575
- // The user might benefit from using a Periodic Table of Elements. Provided on Khan Academy when true
43576
- "periodicTable",
43577
- // The user might benefit from using a Periodic Table of Elements with key. Provided on Khan Academy when true
43578
- "periodicTableWithKey",
43579
- // The user might benefit from using a statistics T Table like https://www.statisticshowto.com/tables/t-distribution-table/
43580
- "tTable",
43581
- // The user might benefit from using a statistics Z Table like https://www.ztable.net/
43582
- "zTable"];
43583
-
43584
- // prettier-ignore
43585
-
43586
- // prettier-ignore
43587
-
43588
- // prettier-ignore
43589
-
43590
- // prettier-ignore
43591
-
43592
- // prettier-ignore
43593
-
43594
- // prettier-ignore
43595
-
43596
- // prettier-ignore
43597
-
43598
- // prettier-ignore
43599
-
43600
- // prettier-ignore
43601
-
43602
- // prettier-ignore
43603
-
43604
- // prettier-ignore
43605
-
43606
- // prettier-ignore
43607
-
43608
- // prettier-ignore
43609
-
43610
- // prettier-ignore
43611
-
43612
- // prettier-ignore
43613
-
43614
- // prettier-ignore
43615
-
43616
- // prettier-ignore
43617
-
43618
- // prettier-ignore
43619
-
43620
- // prettier-ignore
43621
-
43622
- // prettier-ignore
43623
-
43624
- // prettier-ignore
43625
-
43626
- // prettier-ignore
43627
-
43628
- // prettier-ignore
43629
-
43630
- // prettier-ignore
43631
-
43632
- // prettier-ignore
43633
-
43634
- // prettier-ignore
43635
-
43636
- // prettier-ignore
43637
-
43638
- // prettier-ignore
43639
-
43640
- // prettier-ignore
43641
-
43642
- // prettier-ignore
43643
-
43644
- // prettier-ignore
43645
-
43646
- // prettier-ignore
43647
-
43648
- // prettier-ignore
43649
-
43650
- // prettier-ignore
43651
-
43652
- // prettier-ignore
43653
-
43654
- // prettier-ignore
43655
-
43656
- //prettier-ignore
43657
-
43658
- // A background image applied to various widgets.
43659
-
43660
- const PerseusExpressionAnswerFormConsidered = ["correct", "wrong", "ungraded"];
43661
-
43662
- // 2D range: xMin, xMax, yMin, yMax
43663
-
43664
- const lockedFigureColors = ["purple", "blue", "teal", "green", "gold", "red", "pink"];
43665
- const plotterPlotTypes = ["bar", "line", "pic", "histogram", "dotplot"];
43666
-
43667
44051
  /**
43668
44052
  * Traverses a {content, widgets, images} renderer props object,
43669
44053
  * such as `itemData.question`
@@ -44272,7 +44656,9 @@ class MultiRenderer extends React.Component {
44272
44656
  // `<Renderer>`. I'm not sure how else to do this.
44273
44657
  const _this$props = this.props,
44274
44658
  otherProps = _objectWithoutPropertiesLoose(_this$props, _excluded);
44275
- return otherProps;
44659
+ return _extends({}, otherProps, {
44660
+ strings: this.context.strings
44661
+ });
44276
44662
  }
44277
44663
 
44278
44664
  /**
@@ -44523,7 +44909,7 @@ class MultiRenderer extends React.Component {
44523
44909
  if (this.state.renderError) {
44524
44910
  return /*#__PURE__*/React.createElement("div", {
44525
44911
  className: css(styles.error)
44526
- }, i18n.$_("Error rendering: %(error)s", {
44912
+ }, this.context.strings.errorRendering({
44527
44913
  error: String(this.state.renderError)
44528
44914
  }));
44529
44915
  }
@@ -44537,6 +44923,7 @@ class MultiRenderer extends React.Component {
44537
44923
  }));
44538
44924
  }
44539
44925
  }
44926
+ MultiRenderer.contextType = PerseusI18nContext;
44540
44927
  const styles = StyleSheet.create({
44541
44928
  error: {
44542
44929
  color: "red"
@@ -45632,5 +46019,5 @@ const EditorJsonify = {
45632
46019
  }
45633
46020
  };
45634
46021
 
45635
- export { ApiOptions, ArrowPicker, ArticleRenderer, BaseRadio, Categorizer$1 as Categorizer, changeable as Changeable, ClassNames, ColorPicker, DashPicker, dependencies as Dependencies, EditorJsonify, ElementContainer, Errors, Expression, util as GrapherUtil, Grapher$1 as GrapherWidget, HintRenderer, HintsRenderer, InputNumber$1 as InputNumber, InteractiveGraph$1 as InteractiveGraphWidget, ItemExtras, JiptParagraphs, KhanColors, KhanMath, context as LoadingContext, Log, Matrix$1 as MatrixWidget, MultiItems, NumericInput$1 as NumericInput, PerseusError, PerseusExpressionAnswerFormConsidered, PerseusMarkdown, Plotter$1 as PlotterWidget, QUESTION_WIDGETS, Radio, Renderer, ref as ServerItemRenderer, sizingUtils as SizingUtils, Table$1 as TableWidget, Util, denylist as WIDGET_PROP_DENYLIST, WidgetJsonifyDeprecated, widgets$1 as Widgets, addWidget, apiVersion, buildEmptyItemTreeForShape, buildMapper, components, constants, contentHasWidgetType, convertWidgetNameToEnum, displaySigFigs, generateTestCategorizerWidget, generateTestExpressionWidget, generateTestInteractiveGraphWidget, generateTestNumericInputWidget, generateTestPerseusItem, generateTestRadioWidget, genericPerseusItemData, getAnswerFromUserInput, getAnswersFromWidgets, getCorrectAnswerForWidgetId, getValidWidgetIds, getWidgetFromWidgetMap, getWidgetIdsFromContent, getWidgetIdsFromContentByType, getWidgetTypeByWidgetId, getWidgetsFromWidgetMap, getWidgetsMapFromItemData, globalConstants, globalStyles, iconPaths as icons, init, injectWidgets, interactiveSizes$1 as interactiveSizes, isItemRenderableByVersion, isWidgetIdInContent, isWrongAnswerSupported, itemDataVersion, itemToTree, ItemVersion as itemVersion, libVersion, lockedFigureColors, mediaQueries, parsePerseusItem, plotterPlotTypes, preprocessTex, registerAllWidgetsForTesting, shapes, shouldHaveIndividualAnswer, traverse, violatingWidgets, allWidgets as widgets };
46022
+ export { ApiOptions, ArrowPicker, ArticleRenderer, BaseRadio, Categorizer$1 as Categorizer, changeable as Changeable, ClassNames, ColorPicker, DashPicker, dependencies as Dependencies, EditorJsonify, ElementContainer, Errors, Expression, util as GrapherUtil, Grapher$1 as GrapherWidget, HintRenderer, HintsRenderer, InputNumber$1 as InputNumber, InteractiveGraph$1 as InteractiveGraphWidget, ItemExtras, JiptParagraphs, KhanColors, KhanMath, context as LoadingContext, Log, Matrix$1 as MatrixWidget, MultiItems, NumericInput$1 as NumericInput, PerseusError, PerseusExpressionAnswerFormConsidered, PerseusI18nContext, PerseusI18nContextProvider, PerseusMarkdown, Plotter$1 as PlotterWidget, QUESTION_WIDGETS, Radio, Renderer, ref as ServerItemRenderer, sizingUtils as SizingUtils, Table$1 as TableWidget, Util, denylist as WIDGET_PROP_DENYLIST, WidgetJsonifyDeprecated, widgets$1 as Widgets, addWidget, apiVersion, buildEmptyItemTreeForShape, buildMapper, components, constants, contentHasWidgetType, convertWidgetNameToEnum, displaySigFigs, generateTestCategorizerWidget, generateTestExpressionWidget, generateTestInteractiveGraphWidget, generateTestNumericInputWidget, generateTestPerseusItem, generateTestRadioWidget, genericPerseusItemData, getAnswerFromUserInput, getAnswersFromWidgets, getCorrectAnswerForWidgetId, getValidWidgetIds, getWidgetFromWidgetMap, getWidgetIdsFromContent, getWidgetIdsFromContentByType, getWidgetTypeByWidgetId, getWidgetsFromWidgetMap, getWidgetsMapFromItemData, globalConstants, globalStyles, iconPaths as icons, init, injectWidgets, interactiveSizes$1 as interactiveSizes, isItemRenderableByVersion, isWidgetIdInContent, isWrongAnswerSupported, itemDataVersion, itemToTree, ItemVersion as itemVersion, libVersion, lockedFigureColors, mediaQueries, parsePerseusItem, plotterPlotTypes, preprocessTex, registerAllWidgetsForTesting, shapes, shouldHaveIndividualAnswer, traverse, usePerseusI18n, violatingWidgets, allWidgets as widgets };
45636
46023
  //# sourceMappingURL=index.js.map