@khanacademy/perseus 21.5.0 → 22.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) 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.css +3 -0
  9. package/dist/es/index.js +953 -530
  10. package/dist/es/index.js.map +1 -1
  11. package/dist/es/strings.js +312 -0
  12. package/dist/es/strings.js.map +1 -0
  13. package/dist/hint-renderer.d.ts +6 -0
  14. package/dist/hints-renderer.d.ts +6 -0
  15. package/dist/index.css +3 -0
  16. package/dist/index.d.ts +3 -2
  17. package/dist/index.js +1175 -565
  18. package/dist/index.js.map +1 -1
  19. package/dist/multi-items/multi-renderer.d.ts +6 -0
  20. package/dist/perseus-api.d.ts +5 -3
  21. package/dist/perseus-types.d.ts +18 -8
  22. package/dist/renderer.d.ts +2 -0
  23. package/dist/server-item-renderer.d.ts +11 -3
  24. package/dist/strings.d.ts +190 -0
  25. package/dist/strings.js +395 -119
  26. package/dist/strings.js.map +1 -0
  27. package/dist/types.d.ts +10 -6
  28. package/dist/util/answer-types.d.ts +4 -12
  29. package/dist/util.d.ts +2 -1
  30. package/dist/widgets/categorizer.d.ts +8 -1
  31. package/dist/widgets/definition.d.ts +6 -0
  32. package/dist/widgets/deprecated-standin/index.d.ts +6 -0
  33. package/dist/widgets/explanation.d.ts +6 -0
  34. package/dist/widgets/expression.d.ts +42 -19
  35. package/dist/widgets/graded-group-answer-bar.d.ts +6 -0
  36. package/dist/widgets/graded-group.d.ts +6 -0
  37. package/dist/widgets/group.d.ts +6 -0
  38. package/dist/widgets/image.d.ts +6 -0
  39. package/dist/widgets/input-number.d.ts +8 -1
  40. package/dist/widgets/interactive-graph.d.ts +6 -0
  41. package/dist/widgets/interactive-graphs/interactive-graph-question-builder.d.ts +21 -0
  42. package/dist/widgets/interactive-graphs/locked-line.d.ts +4 -0
  43. package/dist/widgets/interactive-graphs/locked-point.d.ts +4 -0
  44. package/dist/widgets/interactive-graphs/mafs-graph.d.ts +1 -0
  45. package/dist/widgets/interactive-graphs/reducer/use-graph-config.d.ts +1 -0
  46. package/dist/widgets/label-image/marker.d.ts +6 -0
  47. package/dist/widgets/label-image.d.ts +16 -6
  48. package/dist/widgets/matcher.d.ts +6 -0
  49. package/dist/widgets/matrix.d.ts +9 -1
  50. package/dist/widgets/molecule.d.ts +6 -0
  51. package/dist/widgets/number-line.d.ts +6 -0
  52. package/dist/widgets/numeric-input.d.ts +8 -1
  53. package/dist/widgets/passage-ref-target.d.ts +6 -0
  54. package/dist/widgets/passage-ref.d.ts +6 -0
  55. package/dist/widgets/passage.d.ts +6 -0
  56. package/dist/widgets/plotter.d.ts +6 -0
  57. package/dist/widgets/radio/choice-a11y-text.d.ts +2 -1
  58. package/dist/widgets/radio/radio.d.ts +8 -1
  59. package/dist/widgets/radio/util.d.ts +2 -1
  60. package/dist/widgets/table.d.ts +7 -0
  61. package/dist/widgets/unit.d.ts +8 -1
  62. package/dist/widgets/video.d.ts +6 -0
  63. package/dist/widgets.d.ts +2 -1
  64. package/package.json +50 -39
  65. 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';
@@ -30,7 +29,7 @@ import { OptionItem, SingleSelect, MultiSelect } from '@khanacademy/wonder-block
30
29
  import caretDown from '@phosphor-icons/core/assets/regular/caret-down.svg';
31
30
  import caretUp from '@phosphor-icons/core/assets/regular/caret-up.svg';
32
31
  import debounce$2 from 'lodash.debounce';
33
- import { Point as Point$2, useTransformContext, vec, useMovable, Text as Text$1, Polygon, Coordinates, Mafs } from 'mafs';
32
+ import { Line as Line$3, Point as Point$2, useTransformContext, vec, useMovable, Text as Text$1, Polygon, Coordinates, Mafs } from 'mafs';
34
33
  import { PhosphorIcon } from '@khanacademy/wonder-blocks-icon';
35
34
  import questionIcon from '@phosphor-icons/core/regular/question.svg';
36
35
  import { LabelSmall, LabelMedium } from '@khanacademy/wonder-blocks-typography';
@@ -45,14 +44,12 @@ function _extends() {
45
44
  _extends = Object.assign ? Object.assign.bind() : function (target) {
46
45
  for (var i = 1; i < arguments.length; i++) {
47
46
  var source = arguments[i];
48
-
49
47
  for (var key in source) {
50
48
  if (Object.prototype.hasOwnProperty.call(source, key)) {
51
49
  target[key] = source[key];
52
50
  }
53
51
  }
54
52
  }
55
-
56
53
  return target;
57
54
  };
58
55
  return _extends.apply(this, arguments);
@@ -63,16 +60,219 @@ function _objectWithoutPropertiesLoose(source, excluded) {
63
60
  var target = {};
64
61
  var sourceKeys = Object.keys(source);
65
62
  var key, i;
66
-
67
63
  for (i = 0; i < sourceKeys.length; i++) {
68
64
  key = sourceKeys[i];
69
65
  if (excluded.indexOf(key) >= 0) continue;
70
66
  target[key] = source[key];
71
67
  }
72
-
73
68
  return target;
74
69
  }
75
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
+
76
276
  const debounce = (func, delay) => {
77
277
  let timer = null;
78
278
  return (...args) => {
@@ -85,21 +285,8 @@ const debounce = (func, delay) => {
85
285
  };
86
286
  };
87
287
 
88
- const customKeyTranslator = _extends({}, keyTranslator, {
89
- // If there's something in the input that can become part of a
90
- // fraction, typing "/" puts it in the numerator. If not, typing
91
- // "/" does nothing. In that case, enter a \frac.
92
- FRAC: mathQuill => {
93
- const contents = mathQuill.latex();
94
- mathQuill.typedText("/");
95
- if (mathQuill.latex() === contents) {
96
- mathQuill.cmd("\\frac");
97
- }
98
- }
99
- });
100
-
101
288
  // A WYSIWYG math input that calls `onChange(LaTeX-string)`
102
- class MathInput extends React.Component {
289
+ class InnerMathInput extends React.Component {
103
290
  constructor(...args) {
104
291
  super(...args);
105
292
  // @ts-expect-error - TS2564 - Property 'mouseDown' has no initializer and is not definitely assigned in the constructor.
@@ -113,6 +300,21 @@ class MathInput extends React.Component {
113
300
  };
114
301
  this.insert = value => {
115
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
+ });
116
318
  const inputModifier = customKeyTranslator[value];
117
319
  if (inputModifier) {
118
320
  inputModifier(input, value);
@@ -135,8 +337,11 @@ class MathInput extends React.Component {
135
337
  };
136
338
  this.mathField = () => {
137
339
  if (!this.__mathField && this.__mathFieldWrapperRef) {
340
+ const {
341
+ locale
342
+ } = this.context;
138
343
  // Initialize MathQuill.MathField instance
139
- this.__mathField = createMathField(this.__mathFieldWrapperRef, baseConfig => _extends({}, baseConfig, {
344
+ this.__mathField = createMathField(this.__mathFieldWrapperRef, this.props.mathInputStrings, baseConfig => _extends({}, baseConfig, {
140
345
  handlers: {
141
346
  edit: debounce(mathField => {
142
347
  // This handler is guaranteed to be called on change, but
@@ -152,7 +357,7 @@ class MathInput extends React.Component {
152
357
  // Use the specified symbol to represent multiplication
153
358
  // TODO(alex): Add an option to disallow variables, in
154
359
  // which case 'x' should get converted to '\\times'
155
- if (convertDotToTimesByLocale(this.props.convertDotToTimes)) {
360
+ if (convertDotToTimesByLocale(locale, this.props.convertDotToTimes)) {
156
361
  value = value.replace(/\\cdot/g, "\\times");
157
362
 
158
363
  // Preserve cursor position in the common case:
@@ -212,7 +417,10 @@ class MathInput extends React.Component {
212
417
  focused: false
213
418
  });
214
419
  this.handleKeypadPress = (key, e) => {
215
- const translator = keyTranslator[key];
420
+ const {
421
+ locale
422
+ } = this.context;
423
+ const translator = getKeyTranslator(locale)[key];
216
424
  const mathField = this.mathField();
217
425
  if (mathField) {
218
426
  if (translator) {
@@ -311,7 +519,7 @@ class MathInput extends React.Component {
311
519
  focused: false,
312
520
  active: false
313
521
  }) : /*#__PURE__*/React.createElement(Clickable, {
314
- "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,
315
523
  "aria-checked": this.state.keypadOpen,
316
524
  role: "switch",
317
525
  onClick: () => this.state.keypadOpen ? this.closeKeypad() : this.openKeypad()
@@ -320,10 +528,36 @@ class MathInput extends React.Component {
320
528
  }, props))))));
321
529
  }
322
530
  }
323
- 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 = {
324
537
  value: "",
325
538
  convertDotToTimes: false
326
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;
327
561
  const MathInputIcon = ({
328
562
  hovered,
329
563
  focused,
@@ -1239,16 +1473,6 @@ const MAXERROR_EPSILON = Math.pow(2, -42);
1239
1473
  // TOOD(kevinb): Figure out how this relates to KEScore in
1240
1474
  // perseus-all-package/types.js and see if there's a way to
1241
1475
  // unify these types.
1242
- const errors = {
1243
- 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>"),
1244
- EXTRA_SYMBOLS_ERROR: i18n._("We could not understand your " + "answer. Please check your answer for extra " + "text or symbols."),
1245
- NEEDS_TO_BE_SIMPLFIED_ERROR: i18n._("Your answer is almost correct, " + "but it needs to be simplified."),
1246
- MISSING_PERCENT_ERROR: i18n._("Your answer is almost correct, " + "but it is missing a " + "<code>\\%</code> at the end."),
1247
- MULTIPLICATION_SIGN_ERROR: i18n._("I'm a computer. I only understand " + "multiplication if you use an asterisk " + "(*) as the multiplication sign."),
1248
- WRONG_CASE_ERROR: i18n._("Your answer includes use of a variable with the wrong case."),
1249
- WRONG_LETTER_ERROR: i18n._("Your answer includes a wrong variable letter.")
1250
- };
1251
-
1252
1476
  /*
1253
1477
  * Answer types
1254
1478
  *
@@ -1321,7 +1545,7 @@ const KhanAnswerTypes = {
1321
1545
  */
1322
1546
  predicate: {
1323
1547
  defaultForms: "integer, proper, improper, mixed, decimal",
1324
- createValidatorFunctional: function (predicate, options) {
1548
+ createValidatorFunctional: function (predicate, options, strings) {
1325
1549
  // Extract the options from the given solution object
1326
1550
  options = _.extend({
1327
1551
  simplify: "required",
@@ -1709,12 +1933,12 @@ const KhanAnswerTypes = {
1709
1933
  } else if (form === "percent") {
1710
1934
  // Otherwise, an error was returned
1711
1935
  score.empty = true;
1712
- score.message = errors.MISSING_PERCENT_ERROR;
1936
+ score.message = strings.MISSING_PERCENT_ERROR;
1713
1937
  } else {
1714
1938
  if (options.simplify !== "enforced") {
1715
1939
  score.empty = true;
1716
1940
  }
1717
- score.message = errors.NEEDS_TO_BE_SIMPLFIED_ERROR;
1941
+ score.message = strings.NEEDS_TO_BE_SIMPLFIED_ERROR;
1718
1942
  }
1719
1943
  // The return false below stops the looping of the
1720
1944
  // callback since predicate check succeeded.
@@ -1723,7 +1947,7 @@ const KhanAnswerTypes = {
1723
1947
  }
1724
1948
  if (piApprox && predicate(val, Math.abs(val * 0.001))) {
1725
1949
  score.empty = true;
1726
- score.message = errors.APPROXIMATED_PI_ERROR;
1950
+ score.message = strings.APPROXIMATED_PI_ERROR;
1727
1951
  }
1728
1952
  }
1729
1953
  });
@@ -1739,7 +1963,7 @@ const KhanAnswerTypes = {
1739
1963
  });
1740
1964
  if (!interpretedGuess) {
1741
1965
  score.empty = true;
1742
- score.message = errors.EXTRA_SYMBOLS_ERROR;
1966
+ score.message = strings.EXTRA_SYMBOLS_ERROR;
1743
1967
  return score;
1744
1968
  }
1745
1969
  }
@@ -1762,10 +1986,10 @@ const KhanAnswerTypes = {
1762
1986
  type: "predicate"
1763
1987
  })];
1764
1988
  },
1765
- createValidatorFunctional: function (correctAnswer, options) {
1989
+ createValidatorFunctional: function (correctAnswer, options, strings) {
1766
1990
  return KhanAnswerTypes.predicate.createValidatorFunctional(
1767
1991
  // @ts-expect-error - TS2556 - A spread argument must either have a tuple type or be passed to a rest parameter.
1768
- ...KhanAnswerTypes.number.convertToPredicate(correctAnswer, options));
1992
+ ...KhanAnswerTypes.number.convertToPredicate(correctAnswer, options), strings);
1769
1993
  }
1770
1994
  },
1771
1995
  /*
@@ -1830,7 +2054,7 @@ const KhanAnswerTypes = {
1830
2054
  }
1831
2055
  return solution;
1832
2056
  },
1833
- createValidatorFunctional: function (solution, options) {
2057
+ createValidatorFunctional: function (solution, options, strings) {
1834
2058
  return function (guess) {
1835
2059
  const score = {
1836
2060
  empty: false,
@@ -1881,7 +2105,7 @@ const KhanAnswerTypes = {
1881
2105
  // @ts-expect-error - TS2540 - Cannot assign to 'ungraded' because it is a read-only property.
1882
2106
  score.ungraded = true;
1883
2107
  // @ts-expect-error - TS2540 - Cannot assign to 'message' because it is a read-only property.
1884
- 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;
1885
2109
  // Don't tell the use they're "almost there" in this case, that may not be true and isn't helpful.
1886
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; }'.
1887
2111
  score.suppressAlmostThere = true;
@@ -1907,7 +2131,7 @@ const KhanAnswerTypes = {
1907
2131
  // @ts-expect-error - TS2540 - Cannot assign to 'ungraded' because it is a read-only property.
1908
2132
  score.ungraded = true;
1909
2133
  // @ts-expect-error - TS2540 - Cannot assign to 'message' because it is a read-only property.
1910
- score.message = errors.MULTIPLICATION_SIGN_ERROR;
2134
+ score.message = strings.MULTIPLICATION_SIGN_ERROR;
1911
2135
  } else if (resultX.message) {
1912
2136
  // TODO(aasmund): I18nize `score.message`
1913
2137
  // @ts-expect-error - TS2540 - Cannot assign to 'message' because it is a read-only property.
@@ -1922,9 +2146,6 @@ const KhanAnswerTypes = {
1922
2146
  };
1923
2147
 
1924
2148
  const _excluded$5 = ["scoring"];
1925
- const ERROR_TITLE = i18n._("Oops!");
1926
- const ERROR_MESSAGE = i18n._("Sorry, I don't understand that!");
1927
-
1928
2149
  // Map of international operator names to their English equivalents
1929
2150
  const englishOperators = {
1930
2151
  arctg: "arctan",
@@ -1954,6 +2175,32 @@ const deriveKeypadVersion = apiOptions => {
1954
2175
  // can remove this `virtualKeypadVersion` field entirely.
1955
2176
  return apiOptions.nativeKeypadProxy != null ? "REACT_NATIVE_KEYPAD" : "MATH_INPUT_KEYPAD_V2";
1956
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
+
1957
2204
  // The new, MathQuill input expression widget
1958
2205
  class Expression extends React.Component {
1959
2206
  constructor(..._args) {
@@ -1986,7 +2233,7 @@ class Expression extends React.Component {
1986
2233
  if (!this.parse(this.props.value, this.props).parsed) {
1987
2234
  const apiResult = this.props.apiOptions.onInputError(null,
1988
2235
  // reserved for some widget identifier
1989
- this.props.value, ERROR_MESSAGE);
2236
+ this.props.value, this.context.strings.ERROR_TITLE);
1990
2237
  if (apiResult !== false) {
1991
2238
  this.setState({
1992
2239
  invalid: true
@@ -2010,7 +2257,7 @@ class Expression extends React.Component {
2010
2257
  scoring
2011
2258
  } = _ref,
2012
2259
  rubric = _objectWithoutPropertiesLoose(_ref, _excluded$5);
2013
- 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);
2014
2261
 
2015
2262
  // "scoring" is a flag that indicates when we are checking answers.
2016
2263
  // otherwise, we may just be checking validity after changes.
@@ -2037,7 +2284,7 @@ class Expression extends React.Component {
2037
2284
  // make it so that solution answers with ','s or '.'s work
2038
2285
  const options = _.pick(props || this.props, "functions");
2039
2286
  _.extend(options, {
2040
- decimal_separator: i18n.getDecimalSeparator()
2287
+ decimal_separator: getDecimalSeparator(this.context.locale)
2041
2288
  });
2042
2289
  return KAS.parse(normalizeTex(value), options);
2043
2290
  };
@@ -2115,10 +2362,10 @@ class Expression extends React.Component {
2115
2362
  */
2116
2363
  static validate(userInput, rubric,
2117
2364
  // @ts-expect-error - TS2322 - Type '() => void' is not assignable to type 'OnInputErrorFunctionType'.
2118
- onInputError = function () {}) {
2365
+ onInputError = function () {}, strings, locale) {
2119
2366
  const options = _.clone(rubric);
2120
2367
  _.extend(options, {
2121
- decimal_separator: i18n.getDecimalSeparator()
2368
+ decimal_separator: getDecimalSeparator(locale)
2122
2369
  });
2123
2370
  const createValidator = answer => {
2124
2371
  // We give options to KAS.parse here because it is parsing the
@@ -2141,7 +2388,7 @@ class Expression extends React.Component {
2141
2388
  return KhanAnswerTypes.expression.createValidatorFunctional(expression.expr, _({}).extend(options, {
2142
2389
  simplify: answer.simplify,
2143
2390
  form: answer.form
2144
- }));
2391
+ }), strings);
2145
2392
  };
2146
2393
 
2147
2394
  // Find the first answer form that matches the user's input and that
@@ -2284,6 +2531,10 @@ class Expression extends React.Component {
2284
2531
  "perseus-widget-expression": true,
2285
2532
  "show-error-tooltip": this.state.showErrorTooltip
2286
2533
  });
2534
+ const {
2535
+ ERROR_MESSAGE,
2536
+ ERROR_TITLE
2537
+ } = this.context.strings;
2287
2538
  return /*#__PURE__*/React.createElement("div", {
2288
2539
  className: className,
2289
2540
  onBlur: () => this.state.invalid && this.setState({
@@ -2332,6 +2583,7 @@ class Expression extends React.Component {
2332
2583
  * to be included as keys on the keypad. These are scraped from the answer
2333
2584
  * forms.
2334
2585
  */
2586
+ Expression.contextType = PerseusI18nContext;
2335
2587
  Expression.defaultProps = {
2336
2588
  value: "",
2337
2589
  times: false,
@@ -2719,7 +2971,7 @@ function keScoreFromPerseusScore(score, guess, state) {
2719
2971
  * Return the first valid interpretation of 'text' as a number, in the form
2720
2972
  * {value: 2.3, exact: true}.
2721
2973
  */
2722
- function firstNumericalParse$1(text) {
2974
+ function firstNumericalParse$1(text, strings) {
2723
2975
  // TODO(alpert): This is sort of hacky...
2724
2976
  let first;
2725
2977
  const val = KhanAnswerTypes.predicate.createValidatorFunctional(function (ans) {
@@ -2729,7 +2981,7 @@ function firstNumericalParse$1(text) {
2729
2981
  simplify: "optional",
2730
2982
  inexact: true,
2731
2983
  forms: "integer, proper, improper, pi, log, mixed, decimal"
2732
- });
2984
+ }, strings);
2733
2985
  val(text);
2734
2986
  return first;
2735
2987
  }
@@ -4255,7 +4507,6 @@ function _classPrivateFieldBase(receiver, privateKey) {
4255
4507
  if (!Object.prototype.hasOwnProperty.call(receiver, privateKey)) {
4256
4508
  throw new TypeError("attempted to use private field on non-instance");
4257
4509
  }
4258
-
4259
4510
  return receiver;
4260
4511
  }
4261
4512
 
@@ -10282,8 +10533,6 @@ const getClipPoint = function getClipPoint(graph, coord, angle) {
10282
10533
  class WrappedDrawing {
10283
10534
  constructor(graphie, initialPoint) {
10284
10535
  this.graphie = void 0;
10285
- this.wrapper = void 0;
10286
- this.visibleShape = void 0;
10287
10536
  this.initialPoint = void 0;
10288
10537
  this.graphie = graphie;
10289
10538
  this.initialPoint = initialPoint;
@@ -18317,7 +18566,7 @@ const rules$1 = _extends({}, pureMarkdownRules, {
18317
18566
  // just a stub for testing.
18318
18567
  return /*#__PURE__*/React.createElement("em", {
18319
18568
  key: state.key
18320
- }, i18n.doNotTranslate("[Widget: ", node.id, "]"));
18569
+ }, `[Widget: ${node.id}]`);
18321
18570
  }
18322
18571
  }),
18323
18572
  blockMath: _extends({}, pureMarkdownRules.blockMath, {
@@ -18790,6 +19039,7 @@ const replaceDeprecatedEditors = () => {
18790
19039
  replaceEditor("simulator", "deprecated-standin");
18791
19040
  };
18792
19041
  const getWidget = name => {
19042
+ var _widgets$name;
18793
19043
  // TODO(alex): Consider referring to these as renderers to avoid
18794
19044
  // overloading "widget"
18795
19045
  if (!_.has(widgets, name)) {
@@ -18797,9 +19047,9 @@ const getWidget = name => {
18797
19047
  }
18798
19048
 
18799
19049
  // Allow widgets to specify a widget directly or via a function
18800
- if (widgets[name].getWidget) {
18801
- // @ts-expect-error - TS2722 - Cannot invoke an object which is possibly 'undefined'.
18802
- return widgets[name].getWidget();
19050
+ if ((_widgets$name = widgets[name]) != null && _widgets$name.getWidget) {
19051
+ var _widgets$name$getWidg, _widgets$name2;
19052
+ return (_widgets$name$getWidg = (_widgets$name2 = widgets[name]).getWidget) == null ? void 0 : _widgets$name$getWidg.call(_widgets$name2);
18803
19053
  }
18804
19054
  return widgets[name].widget;
18805
19055
  };
@@ -18948,7 +19198,7 @@ const upgradeWidgetInfoToLatestVersion = oldWidgetInfo => {
18948
19198
  options: newEditorProps
18949
19199
  });
18950
19200
  };
18951
- const getRendererPropsForWidgetInfo = (widgetInfo, problemNum) => {
19201
+ const getRendererPropsForWidgetInfo = (widgetInfo, strings, problemNum) => {
18952
19202
  const type = widgetInfo.type;
18953
19203
  const widgetExports = widgets[type];
18954
19204
  if (widgetExports == null) {
@@ -18969,7 +19219,7 @@ const getRendererPropsForWidgetInfo = (widgetInfo, problemNum) => {
18969
19219
  transform = widgetExports.transform || _.identity;
18970
19220
  }
18971
19221
  // widgetInfo.options are the widgetEditor's props:
18972
- return transform(widgetInfo.options, problemNum);
19222
+ return transform(widgetInfo.options, strings, problemNum);
18973
19223
  };
18974
19224
  const traverseChildWidgets$3 = (widgetInfo, traverseRenderer) => {
18975
19225
  if (!traverseRenderer) {
@@ -19368,8 +19618,11 @@ class Renderer extends React.Component {
19368
19618
  problemNum
19369
19619
  } = props;
19370
19620
  const widgetsStartProps = {};
19621
+ const {
19622
+ strings
19623
+ } = this.props;
19371
19624
  entries(allWidgetInfo).forEach(([key, widgetInfo]) => {
19372
- widgetsStartProps[key] = getRendererPropsForWidgetInfo(widgetInfo, problemNum);
19625
+ widgetsStartProps[key] = getRendererPropsForWidgetInfo(widgetInfo, strings, problemNum);
19373
19626
  });
19374
19627
 
19375
19628
  // Call the onWidgetStartProps callback if it exists
@@ -21429,10 +21682,12 @@ class InputWithExamples extends React.Component {
21429
21682
  id: this._getUniqueId()
21430
21683
  }, /*#__PURE__*/React.createElement(Renderer, {
21431
21684
  content: examplesContent,
21432
- linterContext: PerseusLinter.pushContextStack(this.props.linterContext, "input-with-examples")
21685
+ linterContext: PerseusLinter.pushContextStack(this.props.linterContext, "input-with-examples"),
21686
+ strings: this.context.strings
21433
21687
  })));
21434
21688
  }
21435
21689
  }
21690
+ InputWithExamples.contextType = PerseusI18nContext;
21436
21691
  InputWithExamples.defaultProps = {
21437
21692
  type: "text",
21438
21693
  shouldShowExamples: true,
@@ -21554,32 +21809,32 @@ const answerTypes = {
21554
21809
  }
21555
21810
  };
21556
21811
  const formExamples$1 = {
21557
- integer: function (options) {
21558
- return i18n._("an integer, like $6$");
21812
+ integer: function (options, strings) {
21813
+ return strings.integerExample;
21559
21814
  },
21560
- proper: function (options) {
21815
+ proper: function (options, strings) {
21561
21816
  if (options.simplify === "optional") {
21562
- return i18n._("a *proper* fraction, like $1/2$ or $6/10$");
21817
+ return strings.properExample;
21563
21818
  }
21564
- return i18n._("a *simplified proper* fraction, like $3/5$");
21819
+ return strings.simplifiedProperExample;
21565
21820
  },
21566
- improper: function (options) {
21821
+ improper: function (options, strings) {
21567
21822
  if (options.simplify === "optional") {
21568
- return i18n._("an *improper* fraction, like $10/7$ or $14/8$");
21823
+ return strings.improperExample;
21569
21824
  }
21570
- return i18n._("a *simplified improper* fraction, like $7/4$");
21825
+ return strings.simplifiedImproperExample;
21571
21826
  },
21572
- mixed: function (options) {
21573
- return i18n._("a mixed number, like $1\\ 3/4$");
21827
+ mixed: function (options, strings) {
21828
+ return strings.mixedExample;
21574
21829
  },
21575
- decimal: function (options) {
21576
- return i18n._("an *exact* decimal, like $0.75$");
21830
+ decimal: function (options, strings) {
21831
+ return strings.decimalExample;
21577
21832
  },
21578
- percent: function (options) {
21579
- return i18n._("a percent, like $12.34\\%$");
21833
+ percent: function (options, strings) {
21834
+ return strings.percentExample;
21580
21835
  },
21581
- pi: function (options) {
21582
- return i18n._("a multiple of pi, like $12\\ \\text{pi}$ or " + "$2/3\\ \\text{pi}$");
21836
+ pi: function (options, strings) {
21837
+ return strings.piExample;
21583
21838
  }
21584
21839
  };
21585
21840
  class InputNumber extends React.Component {
@@ -21637,18 +21892,16 @@ class InputNumber extends React.Component {
21637
21892
  };
21638
21893
  this.simpleValidate = (rubric, onInputError) => {
21639
21894
  onInputError = onInputError || function () {};
21640
- return InputNumber.validate(this.getUserInput(), rubric, onInputError);
21895
+ return InputNumber.validate(this.getUserInput(), rubric, this.context.strings, onInputError);
21641
21896
  };
21642
21897
  this.examples = () => {
21898
+ const {
21899
+ strings
21900
+ } = this.context;
21643
21901
  const type = this.props.answerType;
21644
21902
  const forms = answerTypes[type].forms.split(/\s*,\s*/);
21645
- const examples = _.map(forms, function (form) {
21646
- // eslint-disable-next-line @babel/no-invalid-this
21647
- // @ts-expect-error - TS2683 - 'this' implicitly has type 'any' because it does not have a type annotation.
21648
- // eslint-disable-next-line @babel/no-invalid-this
21649
- return formExamples$1[form](this.props);
21650
- }, this);
21651
- return [i18n._("**Your answer should be** ")].concat(examples);
21903
+ const examples = _.map(forms, form => formExamples$1[form](this.props, strings));
21904
+ return [strings.yourAnswer].concat(examples);
21652
21905
  };
21653
21906
  }
21654
21907
  render() {
@@ -21701,7 +21954,7 @@ class InputNumber extends React.Component {
21701
21954
  linterContext: this.props.linterContext
21702
21955
  });
21703
21956
  }
21704
- static validate(state, rubric, onInputError = () => {}) {
21957
+ static validate(state, rubric, strings, onInputError = () => {}) {
21705
21958
  if (rubric.answerType == null) {
21706
21959
  rubric.answerType = "number";
21707
21960
  }
@@ -21715,7 +21968,7 @@ class InputNumber extends React.Component {
21715
21968
  inexact: rubric.inexact || undefined,
21716
21969
  maxError: rubric.maxError,
21717
21970
  forms: answerTypes[rubric.answerType].forms
21718
- });
21971
+ }, strings);
21719
21972
 
21720
21973
  // We may have received TeX; try to parse it before grading.
21721
21974
  // If `currentValue` is not TeX, this should be a no-op.
@@ -21757,6 +22010,7 @@ class InputNumber extends React.Component {
21757
22010
  return answerString;
21758
22011
  }
21759
22012
  }
22013
+ InputNumber.contextType = PerseusI18nContext;
21760
22014
  InputNumber.defaultProps = {
21761
22015
  currentValue: "",
21762
22016
  size: "normal",
@@ -21840,12 +22094,12 @@ const answerFormButtons = [{
21840
22094
  content: "\u03C0"
21841
22095
  }];
21842
22096
  const formExamples = {
21843
- integer: () => i18n._("an integer, like $6$"),
21844
- proper: form => form.simplify === "optional" ? i18n._("a *proper* fraction, like $1/2$ or $6/10$") : i18n._("a *simplified proper* fraction, like $3/5$"),
21845
- improper: form => form.simplify === "optional" ? i18n._("an *improper* fraction, like $10/7$ or $14/8$") : i18n._("a *simplified improper* fraction, like $7/4$"),
21846
- mixed: () => i18n._("a mixed number, like $1\\ 3/4$"),
21847
- decimal: () => i18n._("an *exact* decimal, like $0.75$"),
21848
- pi: () => i18n._("a multiple of pi, like $12\\ \\text{pi}$ or " + "$2/3\\ \\text{pi}$")
22097
+ integer: (form, strings) => strings.integerExample,
22098
+ proper: (form, strings) => form.simplify === "optional" ? strings.properExample : strings.simplifiedProperExample,
22099
+ improper: (form, strings) => form.simplify === "optional" ? strings.improperExample : strings.simplifiedImproperExample,
22100
+ mixed: (form, strings) => strings.mixedExample,
22101
+ decimal: (form, strings) => strings.decimalExample,
22102
+ pi: (form, strings) => strings.piExample
21849
22103
  };
21850
22104
  class NumericInput extends React.Component {
21851
22105
  constructor(...args) {
@@ -21867,13 +22121,12 @@ class NumericInput extends React.Component {
21867
22121
  };
21868
22122
  });
21869
22123
  let examples = _.map(forms, form => {
21870
- // @ts-expect-error - TS2345 - Argument of type 'PerseusNumericInputAnswerForm | { name: string; simplify: string; }' is not assignable to parameter of type 'PerseusNumericInputAnswerForm'.
21871
- return formExamples[form.name](form);
22124
+ return formExamples[form.name](form, this.context.strings);
21872
22125
  });
21873
22126
  // Ensure no duplicate tooltip text from simplified and unsimplified
21874
22127
  // versions of the same format
21875
22128
  examples = _.uniq(examples);
21876
- return [i18n._("**Your answer should be** ")].concat(examples);
22129
+ return [this.context.strings.yourAnswer].concat(examples);
21877
22130
  };
21878
22131
  this.shouldShowExamples = () => {
21879
22132
  var _this$props$answerFor2, _this$props$answerFor3;
@@ -21886,7 +22139,7 @@ class NumericInput extends React.Component {
21886
22139
  return !noFormsAccepted && !allFormsAccepted;
21887
22140
  };
21888
22141
  this.simpleValidate = rubric => {
21889
- return NumericInput.validate(this.getUserInput(), rubric);
22142
+ return NumericInput.validate(this.getUserInput(), rubric, this.context.strings);
21890
22143
  };
21891
22144
  this.focus = () => {
21892
22145
  var _this$inputRef;
@@ -21966,7 +22219,7 @@ class NumericInput extends React.Component {
21966
22219
  }
21967
22220
  return answerStrings[0];
21968
22221
  }
21969
- static validate(userInput, rubric) {
22222
+ static validate(userInput, rubric, strings) {
21970
22223
  const defaultAnswerForms = answerFormButtons.map(e => e["value"])
21971
22224
  // Don't default to validating the answer as a pi answer
21972
22225
  // if answerForm isn't set on the answer
@@ -21993,7 +22246,7 @@ class NumericInput extends React.Component {
21993
22246
  // TODO(merlob) backfill / delete
21994
22247
  maxError: answer.maxError,
21995
22248
  forms: validatorForms
21996
- });
22249
+ }, strings);
21997
22250
  };
21998
22251
 
21999
22252
  // We may have received TeX; try to parse it before grading.
@@ -22063,7 +22316,7 @@ class NumericInput extends React.Component {
22063
22316
  render() {
22064
22317
  let labelText = this.props.labelText;
22065
22318
  if (labelText == null || labelText === "") {
22066
- labelText = i18n._("Your answer:");
22319
+ labelText = this.context.strings.yourAnswerLabel;
22067
22320
  }
22068
22321
 
22069
22322
  // To right align a custom keypad we need to wrap it.
@@ -22118,6 +22371,7 @@ class NumericInput extends React.Component {
22118
22371
  // a given *problem* rather than for each possible [correct/wrong] *answer*.
22119
22372
  // When should two answers to a problem take different answer types?
22120
22373
  // See D27790 for more discussion.
22374
+ NumericInput.contextType = PerseusI18nContext;
22121
22375
  NumericInput.defaultProps = {
22122
22376
  currentValue: "",
22123
22377
  size: "normal",
@@ -22273,18 +22527,21 @@ class PassageRef extends React.Component {
22273
22527
  return !_.isEqual(this.props, nextProps) || !_.isEqual(this.state, nextState);
22274
22528
  }
22275
22529
  render() {
22530
+ const {
22531
+ strings
22532
+ } = this.context;
22276
22533
  const lineRange = this.state.lineRange;
22277
22534
  let lineRangeOutput;
22278
22535
  if (!lineRange) {
22279
- lineRangeOutput = i18n.$_("lines %(lineRange)s", {
22536
+ lineRangeOutput = strings.lineRange({
22280
22537
  lineRange: `?${EN_DASH$1}?`
22281
22538
  });
22282
22539
  } else if (lineRange[0] === lineRange[1]) {
22283
- lineRangeOutput = i18n.$_("line %(lineNumber)s", {
22284
- lineNumber: lineRange[0]
22540
+ lineRangeOutput = strings.lineNumber({
22541
+ lineNumber: String(lineRange[0])
22285
22542
  });
22286
22543
  } else {
22287
- lineRangeOutput = i18n.$_("lines %(lineRange)s", {
22544
+ lineRangeOutput = strings.lineRange({
22288
22545
  lineRange: lineRange[0] + EN_DASH$1 + lineRange[1]
22289
22546
  });
22290
22547
  }
@@ -22326,6 +22583,7 @@ class PassageRef extends React.Component {
22326
22583
  };
22327
22584
  }
22328
22585
  }
22586
+ PassageRef.contextType = PerseusI18nContext;
22329
22587
  PassageRef.defaultProps = {
22330
22588
  passageNumber: 1,
22331
22589
  referenceNumber: 1,
@@ -22689,7 +22947,7 @@ Icon.defaultProps = {
22689
22947
  color: "currentColor"
22690
22948
  };
22691
22949
 
22692
- function getA11yText(letter, checked, correct, crossedOut, showCorrectness) {
22950
+ function getA11yText(letter, checked, correct, crossedOut, showCorrectness, strings) {
22693
22951
  // There are two pieces of metadata we want to add to each a11yText:
22694
22952
  // whether the answer was checked/crossed-out/neither, and whether the
22695
22953
  // answer is correct/incorrect/not-yet-revealed.
@@ -22698,45 +22956,45 @@ function getA11yText(letter, checked, correct, crossedOut, showCorrectness) {
22698
22956
  // we've just enumerated all 9 possibilities as separate strings.
22699
22957
  if (showCorrectness && correct) {
22700
22958
  if (checked) {
22701
- return i18n._("(Choice %(letter)s, Checked, Correct)", {
22959
+ return strings.choiceCheckedCorrect({
22702
22960
  letter
22703
22961
  });
22704
22962
  }
22705
22963
  if (crossedOut) {
22706
- return i18n._("(Choice %(letter)s, Crossed out, Correct)", {
22964
+ return strings.choiceCrossedOutCorrect({
22707
22965
  letter
22708
22966
  });
22709
22967
  }
22710
- return i18n._("(Choice %(letter)s, Correct)", {
22968
+ return strings.choiceCorrect({
22711
22969
  letter
22712
22970
  });
22713
22971
  }
22714
22972
  if (showCorrectness && !correct) {
22715
22973
  if (checked) {
22716
- return i18n._("(Choice %(letter)s, Checked, Incorrect)", {
22974
+ return strings.choiceCheckedIncorrect({
22717
22975
  letter
22718
22976
  });
22719
22977
  }
22720
22978
  if (crossedOut) {
22721
- return i18n._("(Choice %(letter)s, Crossed out, Incorrect)", {
22979
+ return strings.choiceCrossedOutIncorrect({
22722
22980
  letter
22723
22981
  });
22724
22982
  }
22725
- return i18n._("(Choice %(letter)s, Incorrect)", {
22983
+ return strings.choiceIncorrect({
22726
22984
  letter
22727
22985
  });
22728
22986
  }
22729
22987
  if (checked) {
22730
- return i18n._("(Choice %(letter)s, Checked)", {
22988
+ return strings.choiceChecked({
22731
22989
  letter
22732
22990
  });
22733
22991
  }
22734
22992
  if (crossedOut) {
22735
- return i18n._("(Choice %(letter)s, Crossed out)", {
22993
+ return strings.choiceCrossedOut({
22736
22994
  letter
22737
22995
  });
22738
22996
  }
22739
- return i18n._("(Choice %(letter)s)", {
22997
+ return strings.choice({
22740
22998
  letter
22741
22999
  });
22742
23000
  }
@@ -22758,7 +23016,7 @@ const FocusRing = function FocusRing(props) {
22758
23016
  borderRadius
22759
23017
  };
22760
23018
  return /*#__PURE__*/React.createElement("span", {
22761
- "data-test-id": "focus-ring",
23019
+ "data-testid": "focus-ring",
22762
23020
  className: css(styles$z.ring),
22763
23021
  style: style
22764
23022
  }, children);
@@ -22783,18 +23041,8 @@ const styles$z = StyleSheet.create({
22783
23041
  * "choice letter". (For example, in English, `getChoiceLetter(0)` is "A",
22784
23042
  * `getChoiceLetter(1)` is "B", etc.)
22785
23043
  */
22786
- function getChoiceLetter(pos) {
22787
- /* I18N: This is a list of single-character labels that will appear in
22788
- * front of multiple-choice options. For instance, a multiple-choice
22789
- * question with three options would display
22790
- * (A) first option
22791
- * (B) second option
22792
- * (C) third option
22793
- * There must be spaces between each of the different characters. The
22794
- * characters will show up next to options in the order that they are
22795
- * listed here. Most multiple choice questions have 5 or fewer options.
22796
- */
22797
- 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");
23044
+ function getChoiceLetter(pos, strings) {
23045
+ const lettersString = strings.letters;
22798
23046
  const letters = lettersString.split(" ");
22799
23047
  if (pos < letters.length) {
22800
23048
  // If the position we need is listed in the localized string, use that.
@@ -22829,7 +23077,7 @@ const CrossOutLine = function CrossOutLine(props) {
22829
23077
  height: CROSS_OUT_LINE_SIZE,
22830
23078
  viewBox: `0 0 ${CROSS_OUT_LINE_SIZE} ${CROSS_OUT_LINE_SIZE}`,
22831
23079
  className: css(styles$x.crossOutLine),
22832
- "data-test-id": "choice-icon__cross-out-line"
23080
+ "data-testid": "choice-icon__cross-out-line"
22833
23081
  }, /*#__PURE__*/React.createElement("line", {
22834
23082
  // The line stretches from the bottom-left to top-right.
22835
23083
  // We don't quite go to the _very_ corner, because the cute
@@ -22858,7 +23106,10 @@ function ChoiceInner(props) {
22858
23106
  showCorrectness,
22859
23107
  correct
22860
23108
  } = props;
22861
- const letter = getChoiceLetter(pos);
23109
+ const {
23110
+ strings
23111
+ } = usePerseusI18n();
23112
+ const letter = getChoiceLetter(pos, strings);
22862
23113
  if (!showCorrectness) {
22863
23114
  return letter;
22864
23115
  }
@@ -22932,7 +23183,7 @@ const ChoiceIcon = function ChoiceIcon(props) {
22932
23183
  }, /*#__PURE__*/React.createElement("div", {
22933
23184
  // @ts-expect-error - TS2322 - Type '{ backgroundColor: string | null | undefined; borderColor: string; color: string; borderRadius: number; }' is not assignable to type 'Properties<string | number, string & {}>'.
22934
23185
  style: dynamicStyles,
22935
- "data-test-id": "choice-icon__library-choice-icon",
23186
+ "data-testid": "choice-icon__library-choice-icon",
22936
23187
  className: css(styles$w.circle, showCorrectness && correct && styles$w.circleCorrect, showCorrectness && !correct && styles$w.circleIncorrect, showCorrectness && !correct && (checked || previouslyAnswered) && styles$w.circleIncorrectAnswered)
22937
23188
  // used in BaseRadio editMode to check
22938
23189
  // if we actually clicked on the radio icon
@@ -23004,24 +23255,24 @@ const styles$w = StyleSheet.create({
23004
23255
  * ChoiceIcon visualizations but is meant to make the distinctions
23005
23256
  * between the states more immediately clear to users.
23006
23257
  */
23007
- function renderText(checked, correct, crossedOut) {
23258
+ function renderText(checked, correct, crossedOut, strings) {
23008
23259
  if (correct) {
23009
23260
  // For correct answers, we surface checked _or_ crossedOut state,
23010
23261
  // because any interaction with the correct answer is noteworthy!
23011
23262
  if (checked) {
23012
- return i18n._("Correct (selected)");
23263
+ return strings.correctSelected;
23013
23264
  }
23014
23265
  if (crossedOut) {
23015
- return i18n._("Correct (but you crossed it out)");
23266
+ return strings.correctCrossedOut;
23016
23267
  }
23017
- return i18n._("Correct");
23268
+ return strings.correct;
23018
23269
  }
23019
23270
  // But, for incorrect answers, we only surface checked state,
23020
23271
  // because crossing out an incorrect answer is not noteworthy.
23021
23272
  if (checked) {
23022
- return i18n._("Incorrect (selected)");
23273
+ return strings.incorrectSelected;
23023
23274
  }
23024
- return i18n._("Incorrect");
23275
+ return strings.incorrect;
23025
23276
  }
23026
23277
  const OptionStatus = function OptionStatus(props) {
23027
23278
  const {
@@ -23031,6 +23282,9 @@ const OptionStatus = function OptionStatus(props) {
23031
23282
  previouslyAnswered,
23032
23283
  reviewMode
23033
23284
  } = props;
23285
+ const {
23286
+ strings
23287
+ } = usePerseusI18n();
23034
23288
 
23035
23289
  // Option status is shown only in review mode, or for incorrectly
23036
23290
  // answered items.
@@ -23050,7 +23304,7 @@ const OptionStatus = function OptionStatus(props) {
23050
23304
  }
23051
23305
  return /*#__PURE__*/React.createElement("div", {
23052
23306
  className: css(styles$v.text, textStyle)
23053
- }, renderText(checked, correct, crossedOut));
23307
+ }, renderText(checked, correct, crossedOut, strings));
23054
23308
  };
23055
23309
  const styles$v = StyleSheet.create({
23056
23310
  text: {
@@ -23097,6 +23351,9 @@ const Choice = function Choice(props) {
23097
23351
  forwardedRef
23098
23352
  } = props;
23099
23353
  const [isInputFocused, setIsInputFocused] = useState(false);
23354
+ const {
23355
+ strings
23356
+ } = usePerseusI18n();
23100
23357
  useEffect(() => {
23101
23358
  if (isInputFocused && disabled) {
23102
23359
  setIsInputFocused(false);
@@ -23126,8 +23383,8 @@ const Choice = function Choice(props) {
23126
23383
  // However, we don't want to do this when we're in review mode in the
23127
23384
  // content library.
23128
23385
  const showDimmed = !reviewMode && apiOptions.readOnly || crossedOut;
23129
- const letter = getChoiceLetter(pos);
23130
- const a11yText = getA11yText(letter, checked, correct, crossedOut, showCorrectness);
23386
+ const letter = getChoiceLetter(pos, strings);
23387
+ const a11yText = getA11yText(letter, checked, correct, crossedOut, showCorrectness, strings);
23131
23388
  const idFactory = useUniqueIdWithMock(`choice`);
23132
23389
  const choiceId = idFactory.get(letter);
23133
23390
  return /*#__PURE__*/React.createElement("div", {
@@ -23227,15 +23484,15 @@ const Choice = function Choice(props) {
23227
23484
  content: ({
23228
23485
  close
23229
23486
  }) => /*#__PURE__*/React.createElement(PopoverContent, {
23230
- title: i18n._("Cross out"),
23231
- content: i18n._("Cross out option"),
23487
+ title: strings.crossOut,
23488
+ content: strings.crossOutOption,
23232
23489
  closeButtonVisible: true,
23233
23490
  actions: /*#__PURE__*/React.createElement(View, null, /*#__PURE__*/React.createElement(Strut, {
23234
23491
  size: spacing.medium_16
23235
23492
  }), /*#__PURE__*/React.createElement(Button, {
23236
23493
  kind: "primary",
23237
- "aria-label": i18n._("Cross out Choice %(letter)s", {
23238
- letter: getChoiceLetter(pos)
23494
+ "aria-label": strings.crossOutChoice({
23495
+ letter: getChoiceLetter(pos, strings)
23239
23496
  }),
23240
23497
  disabled: apiOptions.readOnly || reviewMode,
23241
23498
  onClick: () => {
@@ -23255,14 +23512,14 @@ const Choice = function Choice(props) {
23255
23512
  }
23256
23513
  close();
23257
23514
  }
23258
- }, crossedOut ? i18n._("Bring back") : i18n._("Cross out")))
23515
+ }, crossedOut ? strings.bringBack : strings.crossOut))
23259
23516
  })
23260
23517
  }, ({
23261
23518
  open
23262
23519
  }) => /*#__PURE__*/React.createElement(Clickable, {
23263
23520
  onClick: open,
23264
- "aria-label": i18n._(`Open menu for Choice %(letter)s`, {
23265
- letter: getChoiceLetter(pos)
23521
+ "aria-label": strings.openMenuForChoice({
23522
+ letter: getChoiceLetter(pos, strings)
23266
23523
  }),
23267
23524
  style: {
23268
23525
  alignSelf: "center",
@@ -23282,7 +23539,7 @@ const Choice = function Choice(props) {
23282
23539
  color: color.offBlack64
23283
23540
  })))), showRationale && /*#__PURE__*/React.createElement("div", {
23284
23541
  className: rationaleClassName,
23285
- "data-test-id": `perseus-radio-rationale-content-${pos}`
23542
+ "data-testid": `perseus-radio-rationale-content-${pos}`
23286
23543
  }, rationale));
23287
23544
  };
23288
23545
  const styles$u = StyleSheet.create({
@@ -23314,6 +23571,9 @@ const ChoiceNoneAbove = function ChoiceNoneAbove(props) {
23314
23571
  forwardedRef
23315
23572
  } = props,
23316
23573
  rest = _objectWithoutPropertiesLoose(props, _excluded$3);
23574
+ const {
23575
+ strings
23576
+ } = usePerseusI18n();
23317
23577
  const choiceProps = _extends({}, rest, {
23318
23578
  content: showContent ? content :
23319
23579
  /*#__PURE__*/
@@ -23329,7 +23589,8 @@ const ChoiceNoneAbove = function ChoiceNoneAbove(props) {
23329
23589
  // just to render a string
23330
23590
  React.createElement(Renderer, {
23331
23591
  key: "noneOfTheAboveRenderer",
23332
- content: i18n._("None of the above")
23592
+ content: strings.noneOfTheAbove,
23593
+ strings: strings
23333
23594
  })
23334
23595
  });
23335
23596
  return /*#__PURE__*/React.createElement(Choice$1, _extends({}, choiceProps, {
@@ -23349,16 +23610,16 @@ const {
23349
23610
 
23350
23611
  // exported for tests
23351
23612
 
23352
- function getInstructionsText(multipleSelect, countChoices, numCorrect) {
23613
+ function getInstructionsText(multipleSelect, countChoices, numCorrect, strings) {
23353
23614
  if (multipleSelect) {
23354
23615
  if (countChoices) {
23355
- return i18n._("Choose %(numCorrect)s answers:", {
23356
- numCorrect: numCorrect
23616
+ return strings.chooseNumAnswers({
23617
+ numCorrect: String(numCorrect)
23357
23618
  });
23358
23619
  }
23359
- return i18n._("Choose all answers that apply:");
23620
+ return strings.chooseAllAnswers;
23360
23621
  }
23361
- return i18n._("Choose 1 answer:");
23622
+ return strings.chooseOneAnswer;
23362
23623
  }
23363
23624
  const BaseRadio = function BaseRadio(props) {
23364
23625
  const {
@@ -23373,6 +23634,9 @@ const BaseRadio = function BaseRadio(props) {
23373
23634
  isLastUsedWidget,
23374
23635
  registerFocusFunction
23375
23636
  } = props;
23637
+ const {
23638
+ strings
23639
+ } = usePerseusI18n();
23376
23640
 
23377
23641
  // useEffect doesn't have previous props
23378
23642
  const prevReviewModeRubric = useRef();
@@ -23479,7 +23743,7 @@ const BaseRadio = function BaseRadio(props) {
23479
23743
  const lastChoiceHighlighted = choices[choices.length - 1].highlighted;
23480
23744
  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));
23481
23745
  const instructionsClassName = classNames("instructions", css(styles$t.instructions, isMobile && styles$t.instructionsMobile));
23482
- const instructions = getInstructionsText(multipleSelect, countChoices, numCorrect);
23746
+ const instructions = getInstructionsText(multipleSelect, countChoices, numCorrect, strings);
23483
23747
  const responsiveClassName = css(styles$t.responsiveFieldset);
23484
23748
  const fieldset = /*#__PURE__*/React.createElement("fieldset", {
23485
23749
  className: `perseus-widget-radio-fieldset ${responsiveClassName}`
@@ -23744,7 +24008,8 @@ class Radio$1 extends React.Component {
23744
24008
  linterContext: _extends({}, this.props.linterContext, {
23745
24009
  // @ts-expect-error - TS2322 - Type '{ blockHighlight: true; contentType: string; highlightLint: boolean; paths: readonly string[]; stack: readonly string[]; }' is not assignable to type 'LinterContextProps'.
23746
24010
  blockHighlight: true
23747
- })
24011
+ }),
24012
+ strings: this.context.strings
23748
24013
  });
23749
24014
  };
23750
24015
  // When `BaseRadio`'s `onChange` handler is called, indicating a change in
@@ -23796,7 +24061,7 @@ class Radio$1 extends React.Component {
23796
24061
  return Radio$1.getUserInputFromProps(this.props);
23797
24062
  };
23798
24063
  this.simpleValidate = rubric => {
23799
- return Radio$1.validate(this.getUserInput(), rubric);
24064
+ return Radio$1.validate(this.getUserInput(), rubric, this.context.strings);
23800
24065
  };
23801
24066
  /**
23802
24067
  * Turn on rationale display for the currently selected choices. Note that
@@ -23857,7 +24122,7 @@ class Radio$1 extends React.Component {
23857
24122
  }
23858
24123
  };
23859
24124
  }
23860
- static validate(userInput, rubric) {
24125
+ static validate(userInput, rubric, strings) {
23861
24126
  const numSelected = userInput.choicesSelected.reduce((sum, selected) => {
23862
24127
  return sum + (selected ? 1 : 0);
23863
24128
  }, 0);
@@ -23870,7 +24135,7 @@ class Radio$1 extends React.Component {
23870
24135
  if (userInput.numCorrect && userInput.numCorrect > 1 && numSelected !== userInput.numCorrect) {
23871
24136
  return {
23872
24137
  type: "invalid",
23873
- message: i18n._("Please choose the correct number of answers.")
24138
+ message: strings.chooseCorrectNum
23874
24139
  };
23875
24140
  // If NOTA and some other answer are checked, ...
23876
24141
  }
@@ -23878,7 +24143,7 @@ class Radio$1 extends React.Component {
23878
24143
  if (userInput.noneOfTheAboveSelected && numSelected > 1) {
23879
24144
  return {
23880
24145
  type: "invalid",
23881
- message: i18n._("'None of the above' may not be selected " + "when other answers are selected.")
24146
+ message: strings.notNoneOfTheAbove
23882
24147
  };
23883
24148
  }
23884
24149
  const correct = userInput.choicesSelected.every((selected, i) => {
@@ -23990,6 +24255,9 @@ class Radio$1 extends React.Component {
23990
24255
  const {
23991
24256
  choices
23992
24257
  } = this.props;
24258
+ const {
24259
+ strings
24260
+ } = this.context;
23993
24261
  let choiceStates;
23994
24262
  if (this.props.static) {
23995
24263
  choiceStates = choices.map(choice => ({
@@ -24041,11 +24309,7 @@ class Radio$1 extends React.Component {
24041
24309
  }));
24042
24310
  }
24043
24311
  const choicesProp = choices.map((choice, i) => {
24044
- const content = choice.isNoneOfTheAbove && !choice.content ?
24045
- // we use i18n._ instead of $_ here because the content
24046
- // sent to a renderer needs to be a string, not a react
24047
- // node (/renderable/fragment).
24048
- i18n._("None of the above") : choice.content;
24312
+ const content = choice.isNoneOfTheAbove && !choice.content ? strings.noneOfTheAbove : choice.content;
24049
24313
  const {
24050
24314
  selected,
24051
24315
  crossedOut,
@@ -24097,6 +24361,7 @@ class Radio$1 extends React.Component {
24097
24361
  });
24098
24362
  }
24099
24363
  }
24364
+ Radio$1.contextType = PerseusI18nContext;
24100
24365
  Radio$1.defaultProps = {
24101
24366
  choices: [],
24102
24367
  multipleSelect: false,
@@ -24111,14 +24376,8 @@ const {
24111
24376
  random
24112
24377
  } = Util;
24113
24378
 
24114
- // Represents choices that we automatically re-order if encountered.
24115
- // Note: these are in the reversed (incorrect) order that we will swap, if
24116
- // found.
24117
- // Note 2: these are internationalized when compared later on.
24118
- const ReversedChoices = [[i18n._("False"), i18n._("True")], [i18n._("No"), i18n._("Yes")]];
24119
-
24120
24379
  // Transforms the choices for display.
24121
- const _choiceTransform = (widgetOptions, problemNum) => {
24380
+ const _choiceTransform = (widgetOptions, strings, problemNum) => {
24122
24381
  const _maybeRandomize = function _maybeRandomize(array) {
24123
24382
  const randomSeed = problemNum === undefined ? random : problemNum;
24124
24383
  // NOTE: `problemNum` will only be set when the radio widget is
@@ -24147,6 +24406,11 @@ const _choiceTransform = (widgetOptions, problemNum) => {
24147
24406
  return newChoices;
24148
24407
  };
24149
24408
  const enforceOrdering = choices => {
24409
+ // Represents choices that we automatically re-order if encountered.
24410
+ // Note: these are in the reversed (incorrect) order that we will swap, if
24411
+ // found.
24412
+ // Note 2: these are internationalized when compared later on.
24413
+ const ReversedChoices = [[strings.false, strings.true], [strings.no, strings.yes]];
24150
24414
  const content = choices.map(c => c.content);
24151
24415
  if (ReversedChoices.some(reversed => _.isEqual(content, reversed))) {
24152
24416
  return [choices[1], choices[0]];
@@ -24172,8 +24436,8 @@ const _choiceTransform = (widgetOptions, problemNum) => {
24172
24436
  // 1) we randomize the order first
24173
24437
  _maybeRandomize(choices)));
24174
24438
  };
24175
- const transform = (widgetOptions, problemNum) => {
24176
- const choices = _choiceTransform(widgetOptions, problemNum);
24439
+ const transform = (widgetOptions, strings, problemNum) => {
24440
+ const choices = _choiceTransform(widgetOptions, strings, problemNum);
24177
24441
  const numCorrect = _.reduce(widgetOptions.choices, function (memo, choice) {
24178
24442
  return choice.correct ? memo + 1 : memo;
24179
24443
  }, 0);
@@ -24259,7 +24523,7 @@ class Categorizer extends React.Component {
24259
24523
  return Categorizer.getUserInputFromProps(this.props);
24260
24524
  };
24261
24525
  this.simpleValidate = rubric => {
24262
- return Categorizer.validate(this.getUserInput(), rubric);
24526
+ return Categorizer.validate(this.getUserInput(), rubric, this.context.strings);
24263
24527
  };
24264
24528
  }
24265
24529
  render() {
@@ -24288,7 +24552,8 @@ class Categorizer extends React.Component {
24288
24552
  key: i
24289
24553
  }, /*#__PURE__*/React.createElement(Renderer, {
24290
24554
  content: category,
24291
- linterContext: this.props.linterContext
24555
+ linterContext: this.props.linterContext,
24556
+ strings: this.context.strings
24292
24557
  }));
24293
24558
  }))), /*#__PURE__*/React.createElement("tbody", null, indexedItems.map(indexedItem => {
24294
24559
  const item = indexedItem[0];
@@ -24300,7 +24565,8 @@ class Categorizer extends React.Component {
24300
24565
  // @ts-expect-error - TS2322 - Type 'string | number' is not assignable to type 'string | undefined'.
24301
24566
  , {
24302
24567
  content: item,
24303
- linterContext: this.props.linterContext
24568
+ linterContext: this.props.linterContext,
24569
+ strings: this.context.strings
24304
24570
  })), self.props.categories.map((catName, catNum) => {
24305
24571
  const selected = self.props.values[itemNum] === catNum;
24306
24572
  return /*#__PURE__*/React.createElement("td", {
@@ -24343,7 +24609,7 @@ class Categorizer extends React.Component {
24343
24609
  this.change("values", values);
24344
24610
  this.props.trackInteraction();
24345
24611
  }
24346
- static validate(userInput, rubric) {
24612
+ static validate(userInput, rubric, strings) {
24347
24613
  let completed = true;
24348
24614
  let allCorrect = true;
24349
24615
  rubric.values.forEach((value, i) => {
@@ -24357,7 +24623,7 @@ class Categorizer extends React.Component {
24357
24623
  if (!completed) {
24358
24624
  return {
24359
24625
  type: "invalid",
24360
- message: i18n._("Make sure you select something for every row.")
24626
+ message: strings.invalidSelection
24361
24627
  };
24362
24628
  }
24363
24629
  return {
@@ -24373,6 +24639,7 @@ class Categorizer extends React.Component {
24373
24639
  }
24374
24640
 
24375
24641
  // TODO(benkomalo): inject page-margin into Perseus instead of hardcoding.
24642
+ Categorizer.contextType = PerseusI18nContext;
24376
24643
  Categorizer.defaultProps = {
24377
24644
  items: [],
24378
24645
  categories: [],
@@ -24698,7 +24965,8 @@ class Definition extends React.Component {
24698
24965
  }, /*#__PURE__*/React.createElement(Renderer, {
24699
24966
  apiOptions: this.props.apiOptions,
24700
24967
  content: this.props.definition,
24701
- widgets: this.props.widgets
24968
+ widgets: this.props.widgets,
24969
+ strings: this.context.strings
24702
24970
  })),
24703
24971
  opened: activeDefinitionId === this.props.widgetId,
24704
24972
  onClose: () => setActiveDefinitionId(null),
@@ -24720,6 +24988,7 @@ class Definition extends React.Component {
24720
24988
  }, this.props.togglePrompt))));
24721
24989
  }
24722
24990
  }
24991
+ Definition.contextType = PerseusI18nContext;
24723
24992
  Definition.defaultProps = {
24724
24993
  togglePrompt: "define me",
24725
24994
  definition: "definition goes here"
@@ -24770,12 +25039,13 @@ class DeprecatedStandin extends React__default.Component {
24770
25039
  paddingBottom: 8
24771
25040
  }
24772
25041
  }, /*#__PURE__*/React__default.createElement(Banner, {
24773
- 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!"),
25042
+ text: this.context.strings.deprecatedStandin,
24774
25043
  kind: "info",
24775
25044
  layout: "full-width"
24776
25045
  }));
24777
25046
  }
24778
25047
  }
25048
+ DeprecatedStandin.contextType = PerseusI18nContext;
24779
25049
  var DeprecatedStandin$1 = {
24780
25050
  name: "deprecated-standin",
24781
25051
  displayName: "Deprecated Standin",
@@ -24970,10 +25240,12 @@ class Explanation extends React.Component {
24970
25240
  apiOptions: this.props.apiOptions,
24971
25241
  content: this.props.explanation,
24972
25242
  widgets: this.props.widgets,
24973
- linterContext: this.props.linterContext
25243
+ linterContext: this.props.linterContext,
25244
+ strings: this.context.strings
24974
25245
  })))));
24975
25246
  }
24976
25247
  }
25248
+ Explanation.contextType = PerseusI18nContext;
24977
25249
  Explanation.defaultProps = {
24978
25250
  showPrompt: "Explain",
24979
25251
  hidePrompt: "Hide explanation",
@@ -25041,6 +25313,13 @@ class GradedGroupAnswerBar extends React.Component {
25041
25313
  onCheckAnswer,
25042
25314
  onNextQuestion
25043
25315
  } = this.props;
25316
+ const {
25317
+ keepTrying,
25318
+ tryAgain,
25319
+ check,
25320
+ correctExcited,
25321
+ nextQuestion
25322
+ } = this.context.strings;
25044
25323
  const answerBarStyle = _extends({}, styles$o.answerBar, {
25045
25324
  backgroundColor: answerBarState === "CORRECT" ? color.offWhite : "white",
25046
25325
  // Center the "Correct!" message only when there's no next question
@@ -25054,10 +25333,10 @@ class GradedGroupAnswerBar extends React.Component {
25054
25333
  style: {
25055
25334
  marginLeft: 8
25056
25335
  }
25057
- }, i18n._("Keep trying"))) : /*#__PURE__*/React.createElement("span", null); // empty span keeps the button on the right side
25336
+ }, keepTrying)) : /*#__PURE__*/React.createElement("span", null); // empty span keeps the button on the right side
25058
25337
 
25059
25338
  if (answerBarState !== "CORRECT") {
25060
- const buttonLabel = answerBarState === "INCORRECT" ? i18n._("Try again") : i18n._("Check");
25339
+ const buttonLabel = answerBarState === "INCORRECT" ? tryAgain : check;
25061
25340
  return /*#__PURE__*/React.createElement("div", {
25062
25341
  style: answerBarStyle
25063
25342
  }, message, /*#__PURE__*/React.createElement(Button, {
@@ -25080,15 +25359,16 @@ class GradedGroupAnswerBar extends React.Component {
25080
25359
  }
25081
25360
  }))), /*#__PURE__*/React.createElement("span", {
25082
25361
  role: "alert",
25083
- "aria-label": i18n._("Correct!"),
25362
+ "aria-label": correctExcited,
25084
25363
  style: {
25085
25364
  marginLeft: 8
25086
25365
  }
25087
- }, i18n._("Correct!"))), onNextQuestion && /*#__PURE__*/React.createElement(Button, {
25366
+ }, correctExcited)), onNextQuestion && /*#__PURE__*/React.createElement(Button, {
25088
25367
  onClick: onNextQuestion
25089
- }, i18n._("Next question")));
25368
+ }, nextQuestion));
25090
25369
  }
25091
25370
  }
25371
+ GradedGroupAnswerBar.contextType = PerseusI18nContext;
25092
25372
  const fontSize = 17;
25093
25373
  const styles$o = {
25094
25374
  answerBar: {
@@ -25142,10 +25422,6 @@ const getNextState = (currentState, answerable) => {
25142
25422
  }
25143
25423
  };
25144
25424
 
25145
- // Prepended to all invalid messages to make the widget messages a bit clearer
25146
- const INVALID_MESSAGE_PREFIX = i18n._("We couldn't grade your answer.");
25147
- const DEFAULT_INVALID_MESSAGE = i18n._("It looks like you left something blank or ") + i18n._("entered in an invalid answer.");
25148
-
25149
25425
  // exports has no 'transform'
25150
25426
 
25151
25427
  // A Graded Group is more or less a Group widget that displays a check
@@ -25197,8 +25473,13 @@ class GradedGroup extends React.Component {
25197
25473
  // eslint-disable-next-line react/no-string-refs
25198
25474
  // @ts-expect-error - TS2339 - Property 'score' does not exist on type 'ReactInstance'.
25199
25475
  const score = this.refs.renderer.score();
25476
+ const {
25477
+ INVALID_MESSAGE_PREFIX,
25478
+ DEFAULT_INVALID_MESSAGE_1,
25479
+ DEFAULT_INVALID_MESSAGE_2
25480
+ } = this.context.strings;
25200
25481
  const status = score.type === "points" ? score.total === score.earned ? GRADING_STATUSES.correct : GRADING_STATUSES.incorrect : GRADING_STATUSES.invalid;
25201
- const message = score.type === "points" ? score.message || "" : score.message ? `${INVALID_MESSAGE_PREFIX} ${score.message}` : `${INVALID_MESSAGE_PREFIX} ${DEFAULT_INVALID_MESSAGE}`;
25482
+ 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}`;
25202
25483
  this.setState({
25203
25484
  status: status,
25204
25485
  message: message,
@@ -25263,8 +25544,7 @@ class GradedGroup extends React.Component {
25263
25544
  color: "#526f03"
25264
25545
  }
25265
25546
  }));
25266
- // @ts-expect-error - TS2322 - Type 'string' is not assignable to type 'null'.
25267
- gradeStatus = i18n._("Correct");
25547
+ gradeStatus = this.context.strings.correct;
25268
25548
  } else if (this.state.status === GRADING_STATUSES.incorrect) {
25269
25549
  // TODO(jeremy): update to a WB colour
25270
25550
  // @ts-expect-error - TS2322 - Type 'Element' is not assignable to type 'null'.
@@ -25273,8 +25553,7 @@ class GradedGroup extends React.Component {
25273
25553
  color: "#ff5454"
25274
25554
  }
25275
25555
  }));
25276
- // @ts-expect-error - TS2322 - Type 'string' is not assignable to type 'null'.
25277
- gradeStatus = i18n._("Incorrect");
25556
+ gradeStatus = this.context.strings.incorrect;
25278
25557
  }
25279
25558
  const mobileClass = this.props.inGradedGroupSet ? css(styles$n.gradedGroupInSet) : css(styles$n.gradedGroup);
25280
25559
  const classes = classNames({
@@ -25303,7 +25582,8 @@ class GradedGroup extends React.Component {
25303
25582
  readOnly
25304
25583
  }),
25305
25584
  onInteractWithWidget: this._onInteractWithWidget,
25306
- linterContext: this.props.linterContext
25585
+ linterContext: this.props.linterContext,
25586
+ strings: this.context.strings
25307
25587
  })), !apiOptions.isMobile && icon && /*#__PURE__*/React.createElement("div", {
25308
25588
  className: "group-icon"
25309
25589
  }, icon), !apiOptions.isMobile && gradeStatus && /*#__PURE__*/React.createElement("div", {
@@ -25317,14 +25597,14 @@ class GradedGroup extends React.Component {
25317
25597
  kind: "secondary",
25318
25598
  disabled: this.props.apiOptions.readOnly,
25319
25599
  onClick: this._checkAnswer
25320
- }, i18n._("Check")), !apiOptions.isMobile && isCorrect && this.props.onNextQuestion && /*#__PURE__*/React.createElement(Button, {
25600
+ }, this.context.strings.check), !apiOptions.isMobile && isCorrect && this.props.onNextQuestion && /*#__PURE__*/React.createElement(Button, {
25321
25601
  kind: "secondary",
25322
25602
  disabled: this.props.apiOptions.readOnly,
25323
25603
  onClick: this.props.onNextQuestion,
25324
25604
  style: {
25325
25605
  marginLeft: 5
25326
25606
  }
25327
- }, i18n._("Next question")), this.props.hint && this.props.hint.content && (this.state.showHint ? /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement("button", {
25607
+ }, this.context.strings.nextQuestion), this.props.hint && this.props.hint.content && (this.state.showHint ? /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement("button", {
25328
25608
  // @ts-expect-error - TS2322 - Type 'string' is not assignable to type 'number | undefined'.
25329
25609
  tabIndex: "0",
25330
25610
  className: css(styles$n.explanationTitle),
@@ -25338,11 +25618,12 @@ class GradedGroup extends React.Component {
25338
25618
  showHint: false
25339
25619
  });
25340
25620
  }
25341
- }, i18n._("Hide explanation")), /*#__PURE__*/React.createElement(Renderer, _extends({}, this.props.hint, {
25621
+ }, this.context.strings.hideExplanation), /*#__PURE__*/React.createElement(Renderer, _extends({}, this.props.hint, {
25342
25622
  // eslint-disable-next-line react/no-string-refs
25343
25623
  ref: "hints-renderer",
25344
25624
  apiOptions: apiOptions,
25345
- linterContext: this.props.linterContext
25625
+ linterContext: this.props.linterContext,
25626
+ strings: this.context.strings
25346
25627
  }))) :
25347
25628
  /*#__PURE__*/
25348
25629
  // Not using Button here bc the styles won't work.
@@ -25360,7 +25641,7 @@ class GradedGroup extends React.Component {
25360
25641
  });
25361
25642
  },
25362
25643
  className: css(styles$n.showHintLink)
25363
- }, i18n._("Explain"))), apiOptions.isMobile && answerBarState !== "HIDDEN" && /*#__PURE__*/React.createElement(GradedGroupAnswerBar, {
25644
+ }, this.context.strings.explain)), apiOptions.isMobile && answerBarState !== "HIDDEN" && /*#__PURE__*/React.createElement(GradedGroupAnswerBar, {
25364
25645
  apiOptions: apiOptions,
25365
25646
  answerBarState: answerBarState,
25366
25647
  onCheckAnswer: this._checkAnswer,
@@ -25368,6 +25649,7 @@ class GradedGroup extends React.Component {
25368
25649
  }));
25369
25650
  }
25370
25651
  }
25652
+ GradedGroup.contextType = PerseusI18nContext;
25371
25653
  GradedGroup.defaultProps = {
25372
25654
  title: "",
25373
25655
  content: "",
@@ -25461,7 +25743,7 @@ class Indicators extends React.Component {
25461
25743
  key: title
25462
25744
  }, /*#__PURE__*/React.createElement(Clickable, {
25463
25745
  role: "button",
25464
- "aria-label": i18n._("Skip to %(title)s", {
25746
+ "aria-label": this.context.strings.skipToTitle({
25465
25747
  title
25466
25748
  }),
25467
25749
  style: styles$m.indicatorButton,
@@ -25477,12 +25759,10 @@ class Indicators extends React.Component {
25477
25759
  style: styles$m.indicatorDotActive
25478
25760
  }, /*#__PURE__*/React.createElement("span", {
25479
25761
  className: css(a11y.srOnly)
25480
- }, i18n._("Current"))))))));
25762
+ }, this.context.strings.current)))))));
25481
25763
  }
25482
25764
  }
25483
-
25484
- // no transform
25485
-
25765
+ Indicators.contextType = PerseusI18nContext; // no transform
25486
25766
  // TODO(jared): find a better name for this :) and for GradedGroup; the names
25487
25767
  // are currently a little confusing.
25488
25768
  class GradedGroupSet extends React.Component {
@@ -25558,7 +25838,7 @@ class GradedGroupSet extends React.Component {
25558
25838
  }
25559
25839
  const currentGroup = this.props.gradedGroups[this.state.currentGroup];
25560
25840
  if (!currentGroup) {
25561
- return /*#__PURE__*/React.createElement("span", null, i18n.doNotTranslate("No current group..."));
25841
+ return /*#__PURE__*/React.createElement("span", null, "No current group...");
25562
25842
  }
25563
25843
  const numGroups = this.props.gradedGroups.length;
25564
25844
  const handleNextQuestion = this.state.currentGroup < numGroups - 1 ? this.handleNextQuestion : null;
@@ -26853,7 +27133,8 @@ class Group extends React.Component {
26853
27133
  findExternalWidgets: this.props.findWidgets,
26854
27134
  reviewMode: !!this.props.reviewModeRubric,
26855
27135
  onInteractWithWidget: onInteractWithWidget,
26856
- linterContext: this.props.linterContext
27136
+ linterContext: this.props.linterContext,
27137
+ strings: this.context.strings
26857
27138
  }), this.props.icon &&
26858
27139
  /*#__PURE__*/
26859
27140
  // @ts-expect-error - TS2339 - Property 'icon' does not exist on type 'Readonly<Props> & Readonly<{ children?: ReactNode; }>'.
@@ -26862,6 +27143,7 @@ class Group extends React.Component {
26862
27143
  }, this.props.icon));
26863
27144
  }
26864
27145
  }
27146
+ Group.contextType = PerseusI18nContext;
26865
27147
  Group.defaultProps = {
26866
27148
  content: "",
26867
27149
  widgets: {},
@@ -27127,7 +27409,8 @@ class ImageWidget extends React.Component {
27127
27409
  }, /*#__PURE__*/React.createElement(Renderer, {
27128
27410
  content: this.props.alt,
27129
27411
  apiOptions: apiOptions,
27130
- linterContext: this.props.linterContext
27412
+ linterContext: this.props.linterContext,
27413
+ strings: this.context.strings
27131
27414
  }));
27132
27415
  }
27133
27416
 
@@ -27187,7 +27470,8 @@ class ImageWidget extends React.Component {
27187
27470
  }, /*#__PURE__*/React.createElement(Renderer, {
27188
27471
  content: _title + this.props.caption,
27189
27472
  apiOptions: apiOptions,
27190
- linterContext: this.props.linterContext
27473
+ linterContext: this.props.linterContext,
27474
+ strings: this.context.strings
27191
27475
  })));
27192
27476
  }
27193
27477
  return /*#__PURE__*/React.createElement("figure", {
@@ -27205,7 +27489,8 @@ class ImageWidget extends React.Component {
27205
27489
  }, /*#__PURE__*/React.createElement(Renderer, {
27206
27490
  content: this.props.title,
27207
27491
  apiOptions: apiOptions,
27208
- linterContext: this.props.linterContext
27492
+ linterContext: this.props.linterContext,
27493
+ strings: this.context.strings
27209
27494
  }));
27210
27495
  }
27211
27496
  if (this.props.caption) {
@@ -27217,7 +27502,8 @@ class ImageWidget extends React.Component {
27217
27502
  }, /*#__PURE__*/React.createElement(Renderer, {
27218
27503
  content: this.props.caption,
27219
27504
  apiOptions: apiOptions,
27220
- linterContext: this.props.linterContext
27505
+ linterContext: this.props.linterContext,
27506
+ strings: this.context.strings
27221
27507
  }));
27222
27508
  }
27223
27509
  return /*#__PURE__*/React.createElement("figure", {
@@ -27236,6 +27522,7 @@ class ImageWidget extends React.Component {
27236
27522
  };
27237
27523
  }
27238
27524
  }
27525
+ ImageWidget.contextType = PerseusI18nContext;
27239
27526
  ImageWidget.defaultProps = {
27240
27527
  alignment: DEFAULT_ALIGNMENT,
27241
27528
  title: "",
@@ -28168,43 +28455,216 @@ Graph.defaultProps = {
28168
28455
  isMobile: false
28169
28456
  };
28170
28457
 
28171
- /**
28172
- * Takes a style object with color names and converts them to actual
28173
- * Wonder Blocks color values. This way, we can use colors and their
28174
- * names in the editor UI without having to convert them back and forth.
28175
- */
28176
- function convertStyle(style) {
28177
- return _extends({}, style, {
28178
- stroke: color[(style == null ? void 0 : style.stroke) || "blue"],
28179
- fill: color[(style == null ? void 0 : style.fill) || "blue"]
28458
+ // TODO(FEI-4010): Remove `Perseus` prefix for all types here
28459
+ // TODO(FEI-4011): Use types generated by https://github.com/jaredly/generate-perseus-flowtypes
28460
+
28461
+ // Range is replaced within this file with Interval, but it is used elsewhere
28462
+ // and exported from the package, so we need to keep it around.
28463
+ // TODO(FEI-5054): Figure out how to get global .d.ts files working with monorepos
28464
+ const ItemExtras = [
28465
+ // The user might benefit from using a Scientific Calculator. Provided on Khan Academy when true
28466
+ "calculator",
28467
+ // The user might benefit from using a statistics Chi Squared Table like https://people.richland.edu/james/lecture/m170/tbl-chi.html
28468
+ "chi2Table",
28469
+ // The user might benefit from a monthly payments calculator. Provided on Khan Academy when true
28470
+ "financialCalculatorMonthlyPayment",
28471
+ // The user might benefit from a total amount calculator. Provided on Khan Academy when true
28472
+ "financialCalculatorTotalAmount",
28473
+ // The user might benefit from a time to pay off calculator. Provided on Khan Academy when true
28474
+ "financialCalculatorTimeToPayOff",
28475
+ // The user might benefit from using a Periodic Table of Elements. Provided on Khan Academy when true
28476
+ "periodicTable",
28477
+ // The user might benefit from using a Periodic Table of Elements with key. Provided on Khan Academy when true
28478
+ "periodicTableWithKey",
28479
+ // The user might benefit from using a statistics T Table like https://www.statisticshowto.com/tables/t-distribution-table/
28480
+ "tTable",
28481
+ // The user might benefit from using a statistics Z Table like https://www.ztable.net/
28482
+ "zTable"];
28483
+
28484
+ // prettier-ignore
28485
+
28486
+ // prettier-ignore
28487
+
28488
+ // prettier-ignore
28489
+
28490
+ // prettier-ignore
28491
+
28492
+ // prettier-ignore
28493
+
28494
+ // prettier-ignore
28495
+
28496
+ // prettier-ignore
28497
+
28498
+ // prettier-ignore
28499
+
28500
+ // prettier-ignore
28501
+
28502
+ // prettier-ignore
28503
+
28504
+ // prettier-ignore
28505
+
28506
+ // prettier-ignore
28507
+
28508
+ // prettier-ignore
28509
+
28510
+ // prettier-ignore
28511
+
28512
+ // prettier-ignore
28513
+
28514
+ // prettier-ignore
28515
+
28516
+ // prettier-ignore
28517
+
28518
+ // prettier-ignore
28519
+
28520
+ // prettier-ignore
28521
+
28522
+ // prettier-ignore
28523
+
28524
+ // prettier-ignore
28525
+
28526
+ // prettier-ignore
28527
+
28528
+ // prettier-ignore
28529
+
28530
+ // prettier-ignore
28531
+
28532
+ // prettier-ignore
28533
+
28534
+ // prettier-ignore
28535
+
28536
+ // prettier-ignore
28537
+
28538
+ // prettier-ignore
28539
+
28540
+ // prettier-ignore
28541
+
28542
+ // prettier-ignore
28543
+
28544
+ // prettier-ignore
28545
+
28546
+ // prettier-ignore
28547
+
28548
+ // prettier-ignore
28549
+
28550
+ // prettier-ignore
28551
+
28552
+ // prettier-ignore
28553
+
28554
+ // prettier-ignore
28555
+
28556
+ //prettier-ignore
28557
+
28558
+ // A background image applied to various widgets.
28559
+
28560
+ const PerseusExpressionAnswerFormConsidered = ["correct", "wrong", "ungraded"];
28561
+ const lockedFigureColors = {
28562
+ blue: "#3D7586",
28563
+ green: "#447A53",
28564
+ gray: "#5D5F66",
28565
+ grayH: "#3B3D45",
28566
+ grayI: "#21242C",
28567
+ purple: "#594094",
28568
+ purpleD: "#8351E8",
28569
+ pink: "#B25071",
28570
+ orange: "#946700",
28571
+ red: "#D92916"
28572
+ };
28573
+ const plotterPlotTypes = ["bar", "line", "pic", "histogram", "dotplot"];
28574
+
28575
+ const LockedLine = props => {
28576
+ const {
28577
+ color: color$1,
28578
+ lineStyle,
28579
+ kind,
28580
+ points,
28581
+ showStartPoint,
28582
+ showEndPoint
28583
+ } = props;
28584
+ const [point1, point2] = points;
28585
+ let line;
28586
+ if (kind === "line" || kind === "segment") {
28587
+ const LineType = kind === "segment" ? Line$3.Segment : Line$3.ThroughPoints;
28588
+ line = /*#__PURE__*/React.createElement(LineType, {
28589
+ point1: point1.coord,
28590
+ point2: point2.coord,
28591
+ color: lockedFigureColors[color$1],
28592
+ style: lineStyle
28593
+ });
28594
+ } else {
28595
+ // TODO(LEMS-1928): Implement rays
28596
+ return null;
28597
+ }
28598
+ return /*#__PURE__*/React.createElement("g", {
28599
+ className: "locked-line"
28600
+ }, line, showStartPoint && /*#__PURE__*/React.createElement(Point$2, {
28601
+ x: point1.coord[0],
28602
+ y: point1.coord[1],
28603
+ svgCircleProps: {
28604
+ style: {
28605
+ fill: point1.filled ? lockedFigureColors[point1.color] : color.white,
28606
+ stroke: lockedFigureColors[point1.color],
28607
+ strokeWidth: spacing.xxxxSmall_2
28608
+ }
28609
+ }
28610
+ }), showEndPoint && /*#__PURE__*/React.createElement(Point$2, {
28611
+ x: point2.coord[0],
28612
+ y: point2.coord[1],
28613
+ svgCircleProps: {
28614
+ style: {
28615
+ fill: point2.filled ? lockedFigureColors[point2.color] : color.white,
28616
+ stroke: lockedFigureColors[point2.color],
28617
+ strokeWidth: spacing.xxxxSmall_2
28618
+ }
28619
+ }
28620
+ }));
28621
+ };
28622
+
28623
+ const LockedPoint = props => {
28624
+ const {
28625
+ color: color$1,
28626
+ coord,
28627
+ filled
28628
+ } = props;
28629
+ const [x, y] = coord;
28630
+ return /*#__PURE__*/React.createElement(Point$2, {
28631
+ x: x,
28632
+ y: y,
28633
+ svgCircleProps: {
28634
+ style: {
28635
+ fill: filled ? lockedFigureColors[color$1] : color.white,
28636
+ stroke: lockedFigureColors[color$1],
28637
+ strokeWidth: spacing.xxxxSmall_2
28638
+ }
28639
+ }
28180
28640
  });
28181
- }
28641
+ };
28642
+
28182
28643
  const GraphLockedLayer = props => {
28183
28644
  const {
28184
28645
  lockedFigures
28185
28646
  } = props;
28186
28647
  return /*#__PURE__*/React.createElement(React.Fragment, null, lockedFigures.map((figure, index) => {
28648
+ let Figure;
28187
28649
  switch (figure.type) {
28188
28650
  case "point":
28189
- const [x, y] = figure.coord;
28190
- return /*#__PURE__*/React.createElement(Point$2, {
28191
- key: `${figure.type}-${index}`,
28192
- x: x,
28193
- y: y,
28194
- svgCircleProps: {
28195
- style: convertStyle(figure.style)
28196
- }
28197
- });
28651
+ Figure = LockedPoint;
28652
+ break;
28653
+ case "line":
28654
+ Figure = LockedLine;
28655
+ break;
28656
+ default:
28657
+ /**
28658
+ * Devlopment-time future-proofing: This should
28659
+ * fail during type-checking if we add a new locked
28660
+ * shape type and forget to handle it in any other
28661
+ * switch case here.
28662
+ */
28663
+ throw new UnreachableCaseError(figure);
28198
28664
  }
28199
-
28200
- /**
28201
- * Devlopment-time future-proofing: This `never` should
28202
- * fail during type-checking if we add a new locked
28203
- * shape type and forget to handle it in any other
28204
- * switch case here.
28205
- */
28206
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
28207
- throw new UnreachableCaseError(figure.type);
28665
+ return /*#__PURE__*/React.createElement(Figure, _extends({
28666
+ key: `${figure.type}-${index}`
28667
+ }, figure));
28208
28668
  }));
28209
28669
  };
28210
28670
 
@@ -28353,7 +28813,7 @@ function SVGLine(props) {
28353
28813
  y2: end[1],
28354
28814
  style: style,
28355
28815
  className: `${className}`,
28356
- "data-test-id": testId
28816
+ "data-testid": testId
28357
28817
  });
28358
28818
  }
28359
28819
 
@@ -28482,7 +28942,7 @@ const MovableLine = props => {
28482
28942
  ref: line,
28483
28943
  tabIndex: 0,
28484
28944
  className: "movable-line",
28485
- "data-test-id": "movable-line",
28945
+ "data-testid": "movable-line",
28486
28946
  style: {
28487
28947
  cursor: dragging ? "grabbing" : "grab"
28488
28948
  }
@@ -28526,7 +28986,8 @@ const MovableLine = props => {
28526
28986
  const defaultGraphConfig = {
28527
28987
  range: [[0, 1], [0, 1]],
28528
28988
  snapStep: [1, 1],
28529
- markings: "none"
28989
+ markings: "none",
28990
+ showTooltips: false
28530
28991
  };
28531
28992
  const GraphConfigContext = /*#__PURE__*/createContext(defaultGraphConfig);
28532
28993
  function useGraphConfig() {
@@ -28537,10 +28998,12 @@ function useGraphConfig() {
28537
28998
  // graph.
28538
28999
  const hitboxSizePx = 48;
28539
29000
  const StyledMovablePoint = props => {
29001
+ var _Object$entries$find$, _Object$entries$find;
28540
29002
  const {
28541
29003
  range,
28542
29004
  snapStep,
28543
- markings
29005
+ markings,
29006
+ showTooltips
28544
29007
  } = useGraphConfig();
28545
29008
  const hitboxRef = useRef(null);
28546
29009
  const {
@@ -28548,6 +29011,10 @@ const StyledMovablePoint = props => {
28548
29011
  onMove,
28549
29012
  color: color$1 = color.blue
28550
29013
  } = props;
29014
+
29015
+ // WB Tooltip requires a color name for the background color.
29016
+ // Since the color in props is a hex value, a reverse lookup is needed.
29017
+ 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";
28551
29018
  const {
28552
29019
  dragging
28553
29020
  } = useMovable({
@@ -28565,26 +29032,27 @@ const StyledMovablePoint = props => {
28565
29032
  const [[_, horizontalStartY]] = useTransform([0, yMin]);
28566
29033
  const [[__, horizontalEndY]] = useTransform([0, yMax]);
28567
29034
  const showHairlines = dragging && markings !== "none";
28568
- return /*#__PURE__*/React.createElement(React.Fragment, null, showHairlines && /*#__PURE__*/React.createElement("g", null, /*#__PURE__*/React.createElement("line", {
29035
+ const hairlines = /*#__PURE__*/React.createElement("g", null, /*#__PURE__*/React.createElement("line", {
28569
29036
  x1: verticalStartX,
28570
29037
  y1: y,
28571
29038
  x2: verticalEndX,
28572
29039
  y2: y,
28573
- stroke: color.green
29040
+ stroke: color$1
28574
29041
  }), /*#__PURE__*/React.createElement("line", {
28575
29042
  x1: x,
28576
29043
  y1: horizontalStartY,
28577
29044
  x2: x,
28578
29045
  y2: horizontalEndY,
28579
- stroke: color.green
28580
- })), /*#__PURE__*/React.createElement("g", {
29046
+ stroke: color$1
29047
+ }));
29048
+ const svgForPoint = /*#__PURE__*/React.createElement("g", {
28581
29049
  ref: hitboxRef,
28582
29050
  className: pointClasses,
28583
29051
  tabIndex: 0,
28584
29052
  style: {
28585
29053
  "--movable-point-color": color$1
28586
29054
  },
28587
- "data-test-id": "movable-point"
29055
+ "data-testid": "movable-point"
28588
29056
  }, /*#__PURE__*/React.createElement("circle", {
28589
29057
  className: "movable-point-hitbox",
28590
29058
  r: hitboxSizePx / 2,
@@ -28609,8 +29077,16 @@ const StyledMovablePoint = props => {
28609
29077
  style: {
28610
29078
  fill: color$1
28611
29079
  },
28612
- "data-test-id": "movable-point__center"
28613
- })));
29080
+ "data-testid": "movable-point__center"
29081
+ }));
29082
+ return /*#__PURE__*/React.createElement(React.Fragment, null, showHairlines && hairlines, showTooltips ? /*#__PURE__*/React.createElement(Tooltip$1, {
29083
+ autoUpdate: true,
29084
+ backgroundColor: wbColorName,
29085
+ content: `(${point[0]}, ${point[1]})`,
29086
+ contentStyle: {
29087
+ color: "white"
29088
+ }
29089
+ }, svgForPoint) : svgForPoint);
28614
29090
  };
28615
29091
 
28616
29092
  const SegmentGraph = props => {
@@ -28628,8 +29104,7 @@ const SegmentGraph = props => {
28628
29104
  },
28629
29105
  onMovePoint: (endpointIndex, destination) => {
28630
29106
  dispatch(moveControlPoint(endpointIndex, destination, i));
28631
- },
28632
- "data-testid": "segment" + i
29107
+ }
28633
29108
  })));
28634
29109
  };
28635
29110
  const SegmentView = props => {
@@ -28659,8 +29134,7 @@ const LinearGraph = props => {
28659
29134
  dispatch
28660
29135
  } = props;
28661
29136
  const {
28662
- coords: lines,
28663
- type
29137
+ coords: lines
28664
29138
  } = props.graphState;
28665
29139
  const colors = ["var(--movable-line-stroke-color)", "var(--mafs-violet)"];
28666
29140
  return /*#__PURE__*/React.createElement(React.Fragment, null, lines == null ? void 0 : lines.map((line, i) => /*#__PURE__*/React.createElement(LineView, {
@@ -28669,10 +29143,7 @@ const LinearGraph = props => {
28669
29143
  onMoveLine: delta => {
28670
29144
  dispatch(moveLine(i, delta));
28671
29145
  },
28672
- onMovePoint: (endpointIndex, destination) => dispatch(moveControlPoint(endpointIndex, destination, i))
28673
- // "linear" or "linear-system" + index
28674
- ,
28675
- "data-testid": type + i,
29146
+ onMovePoint: (endpointIndex, destination) => dispatch(moveControlPoint(endpointIndex, destination, i)),
28676
29147
  stroke: colors[i]
28677
29148
  })));
28678
29149
  };
@@ -28912,7 +29383,6 @@ const PolygonGraph = props => {
28912
29383
  } = props;
28913
29384
  const {
28914
29385
  coords,
28915
- type,
28916
29386
  showAngles,
28917
29387
  showSides,
28918
29388
  range,
@@ -28983,8 +29453,7 @@ const PolygonGraph = props => {
28983
29453
  }), points.map((point, i) => /*#__PURE__*/React.createElement(StyledMovablePoint, {
28984
29454
  key: "point-" + i,
28985
29455
  point: point,
28986
- onMove: destination => dispatch(movePoint(i, destination)),
28987
- "data-testid": type + i
29456
+ onMove: destination => dispatch(movePoint(i, destination))
28988
29457
  })));
28989
29458
  };
28990
29459
 
@@ -29543,7 +30012,7 @@ class NumberInput extends React.Component {
29543
30012
  const placeholder = this.props.placeholder;
29544
30013
  return _.isFinite(placeholder) ? +placeholder : null;
29545
30014
  }
29546
- const result = firstNumericalParse(value);
30015
+ const result = firstNumericalParse(value, this.context.strings);
29547
30016
  return _.isFinite(result) ? result : this.props.value;
29548
30017
  };
29549
30018
  /* Set text input focus to this input */
@@ -29573,7 +30042,7 @@ class NumberInput extends React.Component {
29573
30042
  if (value == null) {
29574
30043
  return true;
29575
30044
  }
29576
- const val = firstNumericalParse(value);
30045
+ const val = firstNumericalParse(value, this.context.strings);
29577
30046
  const checkValidity = this.props.checkValidity;
29578
30047
  return _.isFinite(val) && checkValidity(val);
29579
30048
  };
@@ -29669,6 +30138,7 @@ class NumberInput extends React.Component {
29669
30138
  }
29670
30139
  }
29671
30140
  }
30141
+ NumberInput.contextType = PerseusI18nContext;
29672
30142
  NumberInput.propTypes = {
29673
30143
  value: PropTypes.number,
29674
30144
  format: PropTypes.string,
@@ -30404,7 +30874,8 @@ const MafsGraph = props => {
30404
30874
  value: {
30405
30875
  range: state.range,
30406
30876
  snapStep: state.snapStep,
30407
- markings: props.markings
30877
+ markings: props.markings,
30878
+ showTooltips: !!props.showTooltips
30408
30879
  }
30409
30880
  }, /*#__PURE__*/React.createElement(View, {
30410
30881
  style: {
@@ -31666,9 +32137,9 @@ class LegacyInteractiveGraph extends React.Component {
31666
32137
  // isClickToAddPoints() only applies to points and polygons
31667
32138
  if (this.isClickToAddPoints() && this.state.shouldShowInstructions) {
31668
32139
  if (this.props.graph.type === "point") {
31669
- instructions = i18n._("Click to add points");
32140
+ instructions = this.context.strings.addPoints;
31670
32141
  } else if (this.props.graph.type === "polygon") {
31671
- instructions = i18n._("Click to add vertices");
32142
+ instructions = this.context.strings.addVertices;
31672
32143
  }
31673
32144
  } else {
31674
32145
  instructions = undefined;
@@ -31708,6 +32179,7 @@ class LegacyInteractiveGraph extends React.Component {
31708
32179
  }));
31709
32180
  }
31710
32181
  }
32182
+ LegacyInteractiveGraph.contextType = PerseusI18nContext;
31711
32183
  LegacyInteractiveGraph.defaultProps = {
31712
32184
  labels: ["x", "y"],
31713
32185
  range: [[-10, 10], [-10, 10]],
@@ -31754,7 +32226,8 @@ class InteractiveGraph extends React.Component {
31754
32226
  ref: this.mafsRef,
31755
32227
  gridStep: gridStep,
31756
32228
  snapStep: snapStep,
31757
- box: box
32229
+ box: box,
32230
+ showTooltips: !!this.props.showTooltips
31758
32231
  }));
31759
32232
  }
31760
32233
  return /*#__PURE__*/React.createElement(LegacyInteractiveGraph, _extends({
@@ -33296,6 +33769,9 @@ var globalStyles = /*#__PURE__*/Object.freeze({
33296
33769
  });
33297
33770
 
33298
33771
  const AnswerChoices = props => {
33772
+ const {
33773
+ strings
33774
+ } = usePerseusI18n();
33299
33775
  const onAnswerChange = selected => {
33300
33776
  const {
33301
33777
  choices,
@@ -33313,6 +33789,7 @@ const AnswerChoices = props => {
33313
33789
  value: content,
33314
33790
  label: /*#__PURE__*/React.createElement(Renderer, {
33315
33791
  content: content,
33792
+ strings: strings,
33316
33793
  inline: true
33317
33794
  })
33318
33795
  }));
@@ -33342,14 +33819,13 @@ const AnswerChoices = props => {
33342
33819
  }));
33343
33820
  };
33344
33821
 
33345
- const strings = {
33346
- hideAnswersToggleLabel: i18n._("Hide answer choices")
33347
- };
33348
-
33349
33822
  const HideAnswersToggle = props => {
33350
33823
  const idFactory = useUniqueIdWithMock();
33351
33824
  const switchId = idFactory.get("hide-answers-toggle");
33352
33825
  const labelId = idFactory.get("hide-answers-label");
33826
+ const {
33827
+ strings
33828
+ } = usePerseusI18n();
33353
33829
  return /*#__PURE__*/React.createElement(View, {
33354
33830
  style: styles$g.switchWrapper
33355
33831
  }, /*#__PURE__*/React.createElement(Switch, {
@@ -33390,9 +33866,12 @@ const AnswerPill = props => {
33390
33866
  hovered
33391
33867
  } = props;
33392
33868
  const idFactory = useUniqueIdWithMock();
33393
- const answerString = selectedAnswers.length > 1 ?
33394
- // always need `ngettext` for variable numbers even if we don't use the singular, see https://khanacademy.slack.com/archives/C0918TZ5G/p1700163024293079
33395
- i18n.ngettext("%(num)s answer", "%(num)s answers", selectedAnswers.length) : selectedAnswers[0];
33869
+ const {
33870
+ strings
33871
+ } = usePerseusI18n();
33872
+ const answerString = selectedAnswers.length > 1 ? strings.answers({
33873
+ num: selectedAnswers.length
33874
+ }) : selectedAnswers[0];
33396
33875
 
33397
33876
  // It should no longer be possible to interact with an answer after it
33398
33877
  // has been labeled (answered) correctly.
@@ -33419,6 +33898,7 @@ const AnswerPill = props => {
33419
33898
  style: [style, popperStyle, styles$f.pill, correct && styles$f.correct, incorrect && styles$f.incorrect, (focused || hovered) && BringToFront]
33420
33899
  }, /*#__PURE__*/React.createElement(Renderer, {
33421
33900
  content: answerString,
33901
+ strings: strings,
33422
33902
  inline: true
33423
33903
  })));
33424
33904
  };
@@ -33513,10 +33993,8 @@ class Marker extends React.Component {
33513
33993
  const markerDisabled = showCorrectness === "correct";
33514
33994
  const active = hovered || focused;
33515
33995
  return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(View, {
33516
- style: [styles$e.marker, active && !markerDisabled && styles$e.markerActive]
33517
- // already translated in: /widgets/graded-group.tsx
33518
- ,
33519
- "aria-label": markerDisabled ? i18n._("Correct!") : label
33996
+ style: [styles$e.marker, active && !markerDisabled && styles$e.markerActive],
33997
+ "aria-label": markerDisabled ? this.context.strings.correctExcited : label
33520
33998
  }, this.renderIcon()), !!selected && showAnswer && /*#__PURE__*/React.createElement(AnswerPill, {
33521
33999
  selectedAnswers: selected,
33522
34000
  showCorrectness: showCorrectness,
@@ -33528,6 +34006,7 @@ class Marker extends React.Component {
33528
34006
  }));
33529
34007
  }
33530
34008
  }
34009
+ Marker.contextType = PerseusI18nContext;
33531
34010
  Marker.defaultProps = {
33532
34011
  selected: []
33533
34012
  };
@@ -34099,8 +34578,11 @@ class LabelImage extends React.Component {
34099
34578
  multipleAnswers,
34100
34579
  hideChoicesFromInstructions: hideChoices
34101
34580
  } = this.props;
34102
- 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.");
34103
- const choicesString = i18n._("Choices:");
34581
+ const {
34582
+ strings
34583
+ } = this.context;
34584
+ const promptString = isMobile ? multipleAnswers ? strings.tapMultiple : strings.tapSingle : multipleAnswers ? strings.clickMultiple : strings.clickSingle;
34585
+ const choicesString = strings.choices;
34104
34586
  return /*#__PURE__*/React.createElement("div", {
34105
34587
  className: classNames("perseus-label-image-widget-instructions", css(styles$d.instructions))
34106
34588
  }, /*#__PURE__*/React.createElement("div", {
@@ -34111,7 +34593,8 @@ class LabelImage extends React.Component {
34111
34593
  className: css(styles$d.instructionsChoice),
34112
34594
  key: index
34113
34595
  }, /*#__PURE__*/React.createElement(Renderer, {
34114
- content: choice
34596
+ content: choice,
34597
+ strings: strings
34115
34598
  })))));
34116
34599
  }
34117
34600
  render() {
@@ -34162,6 +34645,7 @@ class LabelImage extends React.Component {
34162
34645
  }));
34163
34646
  }
34164
34647
  }
34648
+ LabelImage.contextType = PerseusI18nContext;
34165
34649
  const styles$d = StyleSheet.create({
34166
34650
  instructions: {
34167
34651
  paddingBottom: 16
@@ -34475,7 +34959,8 @@ class Draggable extends React.Component {
34475
34959
  }, /*#__PURE__*/React.createElement(Renderer, {
34476
34960
  content: this.props.content,
34477
34961
  linterContext: PerseusLinter.pushContextStack(this.props.linterContext, "draggable"),
34478
- onRender: this.props.onRender
34962
+ onRender: this.props.onRender,
34963
+ strings: this.context.strings
34479
34964
  }));
34480
34965
  }
34481
34966
  componentDidUpdate(prevProps) {
@@ -34500,6 +34985,7 @@ class Draggable extends React.Component {
34500
34985
  }
34501
34986
  }
34502
34987
  }
34988
+ Draggable.contextType = PerseusI18nContext;
34503
34989
  Draggable.defaultProps = {
34504
34990
  includePadding: true,
34505
34991
  type: ItemState.STATIC,
@@ -35060,12 +35546,14 @@ class Matcher extends React.Component {
35060
35546
  className: css(styles$b.column, styles$b.columnLabel)
35061
35547
  }, /*#__PURE__*/React.createElement(Renderer, {
35062
35548
  content: this.props.labels[0] || "...",
35063
- linterContext: this.props.linterContext
35549
+ linterContext: this.props.linterContext,
35550
+ strings: this.context.strings
35064
35551
  })), /*#__PURE__*/React.createElement("th", {
35065
35552
  className: css(styles$b.column, styles$b.columnRight, styles$b.columnLabel)
35066
35553
  }, /*#__PURE__*/React.createElement(Renderer, {
35067
35554
  content: this.props.labels[1] || "...",
35068
- linterContext: this.props.linterContext
35555
+ linterContext: this.props.linterContext,
35556
+ strings: this.context.strings
35069
35557
  }))), /*#__PURE__*/React.createElement("tr", {
35070
35558
  className: css(styles$b.row)
35071
35559
  }, /*#__PURE__*/React.createElement("td", {
@@ -35102,6 +35590,7 @@ class Matcher extends React.Component {
35102
35590
  })))));
35103
35591
  }
35104
35592
  }
35593
+ Matcher.contextType = PerseusI18nContext;
35105
35594
  Matcher.propTypes = {
35106
35595
  apiOptions: ApiOptions.propTypes,
35107
35596
  labels: PropTypes.array,
@@ -35387,8 +35876,58 @@ class Matrix extends React.Component {
35387
35876
  };
35388
35877
  };
35389
35878
  this.simpleValidate = rubric => {
35390
- // @ts-expect-error - TS2339 - Property 'validate' does not exist on type 'typeof Matrix'.
35391
- return Matrix.validate(this.getUserInput(), rubric);
35879
+ return Matrix.validate(this.getUserInput(), rubric, this.context.strings);
35880
+ };
35881
+ }
35882
+ static validate(state, rubric, strings) {
35883
+ const solution = rubric.answers;
35884
+ const supplied = state.answers;
35885
+ const solutionSize = getMatrixSize(solution);
35886
+ const suppliedSize = getMatrixSize(supplied);
35887
+ const incorrectSize = solutionSize[0] !== suppliedSize[0] || solutionSize[1] !== suppliedSize[1];
35888
+ const createValidator = KhanAnswerTypes.number.createValidatorFunctional;
35889
+ let message = null;
35890
+ let hasEmptyCell = false;
35891
+ let incorrect = false;
35892
+ _(suppliedSize[0]).times(row => {
35893
+ _(suppliedSize[1]).times(col => {
35894
+ if (supplied[row][col] == null || supplied[row][col].toString().length === 0) {
35895
+ hasEmptyCell = true;
35896
+ }
35897
+ if (!incorrectSize) {
35898
+ const validator = createValidator(solution[row][col], {
35899
+ simplify: true
35900
+ }, strings);
35901
+ const result = validator(supplied[row][col]);
35902
+ if (result.message) {
35903
+ // @ts-expect-error - TS2322 - Type 'string' is not assignable to type 'null'.
35904
+ message = result.message;
35905
+ }
35906
+ if (!result.correct) {
35907
+ incorrect = true;
35908
+ }
35909
+ }
35910
+ });
35911
+ });
35912
+ if (hasEmptyCell) {
35913
+ return {
35914
+ type: "invalid",
35915
+ message: strings.fillAllCells
35916
+ };
35917
+ }
35918
+ if (incorrectSize) {
35919
+ return {
35920
+ type: "points",
35921
+ earned: 0,
35922
+ total: 1,
35923
+ message: null
35924
+ };
35925
+ }
35926
+ return {
35927
+ type: "points",
35928
+ earned: incorrect ? 0 : 1,
35929
+ total: 1,
35930
+ message: message
35392
35931
  };
35393
35932
  }
35394
35933
  componentDidMount() {
@@ -35430,7 +35969,8 @@ class Matrix extends React.Component {
35430
35969
  className: "matrix-prefix"
35431
35970
  }, /*#__PURE__*/React.createElement(Renderer, {
35432
35971
  content: this.props.prefix,
35433
- linterContext: this.props.linterContext
35972
+ linterContext: this.props.linterContext,
35973
+ strings: this.context.strings
35434
35974
  })), /*#__PURE__*/React.createElement("div", {
35435
35975
  className: "matrix-input"
35436
35976
  }, /*#__PURE__*/React.createElement("div", {
@@ -35543,10 +36083,12 @@ class Matrix extends React.Component {
35543
36083
  className: "matrix-suffix"
35544
36084
  }, /*#__PURE__*/React.createElement(Renderer, {
35545
36085
  content: this.props.suffix,
35546
- linterContext: this.props.linterContext
36086
+ linterContext: this.props.linterContext,
36087
+ strings: this.context.strings
35547
36088
  })));
35548
36089
  }
35549
36090
  }
36091
+ Matrix.contextType = PerseusI18nContext;
35550
36092
  Matrix.defaultProps = {
35551
36093
  matrixBoardSize: [3, 3],
35552
36094
  answers: [[]],
@@ -35556,59 +36098,6 @@ Matrix.defaultProps = {
35556
36098
  apiOptions: ApiOptions.defaults,
35557
36099
  linterContext: linterContextDefault
35558
36100
  };
35559
- _.extend(Matrix, {
35560
- validate: function (state, rubric) {
35561
- const solution = rubric.answers;
35562
- const supplied = state.answers;
35563
- const solutionSize = getMatrixSize(solution);
35564
- const suppliedSize = getMatrixSize(supplied);
35565
- const incorrectSize = solutionSize[0] !== suppliedSize[0] || solutionSize[1] !== suppliedSize[1];
35566
- const createValidator = KhanAnswerTypes.number.createValidatorFunctional;
35567
- let message = null;
35568
- let hasEmptyCell = false;
35569
- let incorrect = false;
35570
- _(suppliedSize[0]).times(row => {
35571
- _(suppliedSize[1]).times(col => {
35572
- if (supplied[row][col] == null || supplied[row][col].toString().length === 0) {
35573
- hasEmptyCell = true;
35574
- }
35575
- if (!incorrectSize) {
35576
- const validator = createValidator(solution[row][col], {
35577
- simplify: true
35578
- });
35579
- const result = validator(supplied[row][col]);
35580
- if (result.message) {
35581
- // @ts-expect-error - TS2322 - Type 'string' is not assignable to type 'null'.
35582
- message = result.message;
35583
- }
35584
- if (!result.correct) {
35585
- incorrect = true;
35586
- }
35587
- }
35588
- });
35589
- });
35590
- if (hasEmptyCell) {
35591
- return {
35592
- type: "invalid",
35593
- message: i18n._("Make sure you fill in all cells in the matrix.")
35594
- };
35595
- }
35596
- if (incorrectSize) {
35597
- return {
35598
- type: "points",
35599
- earned: 0,
35600
- total: 1,
35601
- message: null
35602
- };
35603
- }
35604
- return {
35605
- type: "points",
35606
- earned: incorrect ? 0 : 1,
35607
- total: 1,
35608
- message: message
35609
- };
35610
- }
35611
- });
35612
36101
  const propTransform$1 = editorProps => {
35613
36102
  // Remove answers before passing to widget
35614
36103
  const blankAnswers = _(editorProps.matrixBoardSize[0]).times(function () {
@@ -36646,8 +37135,8 @@ class Molecule extends React.Component {
36646
37135
  // eslint-disable-next-line react/no-string-refs
36647
37136
  ,
36648
37137
  ref: "canvas"
36649
- }, i18n.$_("A molecular structure drawing. SMILES notation: %(content)s", {
36650
- content: this.props.smiles
37138
+ }, this.context.strings.molecularDrawing({
37139
+ content: this.props.smiles || ""
36651
37140
  }));
36652
37141
  if (this.state.error) {
36653
37142
  content = /*#__PURE__*/React.createElement("div", {
@@ -36659,6 +37148,7 @@ class Molecule extends React.Component {
36659
37148
  }, content);
36660
37149
  }
36661
37150
  }
37151
+ Molecule.contextType = PerseusI18nContext;
36662
37152
  class MoleculeWidget extends React.Component {
36663
37153
  constructor(...args) {
36664
37154
  super(...args);
@@ -37121,18 +37611,21 @@ class NumberLine extends React.Component {
37121
37611
  };
37122
37612
  }
37123
37613
  render() {
37614
+ const {
37615
+ strings
37616
+ } = this.context;
37124
37617
  const divisionRange = this.props.divisionRange;
37125
37618
  const divRangeString = divisionRange[0] + EN_DASH + divisionRange[1];
37126
37619
  const invalidNumDivisions = this.props.numDivisions < divisionRange[0] || this.props.numDivisions > divisionRange[1];
37127
37620
  const inequalityControls = /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement("input", {
37128
37621
  type: "button",
37129
37622
  className: "simple-button",
37130
- value: i18n._("Switch direction"),
37623
+ value: strings.switchDirection,
37131
37624
  onClick: this.handleReverse
37132
37625
  }), /*#__PURE__*/React.createElement("input", {
37133
37626
  type: "button",
37134
37627
  className: "simple-button",
37135
- value: _(["le", "ge"]).contains(this.props.rel) ? i18n._("Make circle open") : i18n._("Make circle filled"),
37628
+ value: _(["le", "ge"]).contains(this.props.rel) ? strings.circleOpen : strings.circleFilled,
37136
37629
  onClick: this.handleToggleStrict
37137
37630
  }));
37138
37631
  let tickCtrl;
@@ -37143,7 +37636,7 @@ class NumberLine extends React.Component {
37143
37636
  } else {
37144
37637
  Input = NumberInput;
37145
37638
  }
37146
- tickCtrl = /*#__PURE__*/React.createElement("label", null, i18n._("Number of divisions:"), " ", /*#__PURE__*/React.createElement(Input
37639
+ tickCtrl = /*#__PURE__*/React.createElement("label", null, strings.numDivisions, " ", /*#__PURE__*/React.createElement(Input
37147
37640
  // eslint-disable-next-line react/no-string-refs
37148
37641
  , {
37149
37642
  ref: "tick-ctrl",
@@ -37160,13 +37653,14 @@ class NumberLine extends React.Component {
37160
37653
  className: "perseus-widget " + "perseus-widget-interactive-number-line"
37161
37654
  }, tickCtrl, !this.isValid() ? /*#__PURE__*/React.createElement("div", {
37162
37655
  className: "perseus-error"
37163
- }, i18n.doNotTranslate("Invalid number line configuration.")) : this.props.isTickCtrl && invalidNumDivisions ? /*#__PURE__*/React.createElement("div", {
37656
+ }, "Invalid number line configuration.") : this.props.isTickCtrl && invalidNumDivisions ? /*#__PURE__*/React.createElement("div", {
37164
37657
  className: "perseus-error"
37165
- }, i18n.$_("Please make sure the number of divisions is " + "in the range %(divRangeString)s.", {
37658
+ }, strings.divisions({
37166
37659
  divRangeString: divRangeString
37167
37660
  })) : this._renderGraphie(), !this.props.static && this.props.isInequality && inequalityControls);
37168
37661
  }
37169
37662
  }
37663
+ NumberLine.contextType = PerseusI18nContext;
37170
37664
  NumberLine.defaultProps = {
37171
37665
  range: [0, 10],
37172
37666
  labelStyle: "decimal",
@@ -37393,7 +37887,8 @@ class Card extends React.Component {
37393
37887
  }, /*#__PURE__*/React.createElement("div", {
37394
37888
  className: className.join(" ")
37395
37889
  }, /*#__PURE__*/React.createElement(Renderer, _extends({}, rendererProps, {
37396
- linterContext: this.props.linterContext
37890
+ linterContext: this.props.linterContext,
37891
+ strings: this.context.strings
37397
37892
  }))));
37398
37893
  }
37399
37894
  shouldComponentUpdate(nextProps, nextState) {
@@ -37446,6 +37941,7 @@ class Card extends React.Component {
37446
37941
  document.removeEventListener("touchmove", this.onMouseMove);
37447
37942
  }
37448
37943
  }
37944
+ Card.contextType = PerseusI18nContext;
37449
37945
  Card.propTypes = {
37450
37946
  floating: PropTypes.bool.isRequired,
37451
37947
  animating: PropTypes.bool,
@@ -38735,7 +39231,7 @@ class HighlightSetRenderer extends React.PureComponent {
38735
39231
  }
38736
39232
  const hoveredHighlight = this.props.highlights[hoveredHighlightKey];
38737
39233
  return /*#__PURE__*/React.createElement(HighlightTooltip, {
38738
- label: i18n._("Remove highlight"),
39234
+ label: this.context.strings.removeHighlight,
38739
39235
  focusNode: hoveredHighlight.domRange.endContainer,
38740
39236
  focusOffset: hoveredHighlight.domRange.endOffset,
38741
39237
  offsetParent: this.props.offsetParent,
@@ -38765,6 +39261,7 @@ class HighlightSetRenderer extends React.PureComponent {
38765
39261
  })), this.props.editable && this._renderTooltip());
38766
39262
  }
38767
39263
  }
39264
+ HighlightSetRenderer.contextType = PerseusI18nContext;
38768
39265
 
38769
39266
  /* eslint-disable react/no-unsafe */
38770
39267
  class SelectionTracker extends React.PureComponent {
@@ -38965,7 +39462,7 @@ class HighlightingUI extends React.PureComponent {
38965
39462
  onRemoveHighlight: this.props.onRemoveHighlight,
38966
39463
  zIndexes: this.props.zIndexes
38967
39464
  }), this._selectionIsValid(trackedSelection) && !userIsMouseSelecting && /*#__PURE__*/React.createElement(HighlightTooltip, {
38968
- label: i18n._("Add highlight"),
39465
+ label: this.context.strings.addHighlight,
38969
39466
  onClick: () => this._handleAddHighlight(trackedSelection.proposedHighlight),
38970
39467
  focusNode: trackedSelection.focusNode,
38971
39468
  focusOffset: trackedSelection.focusOffset,
@@ -38973,6 +39470,7 @@ class HighlightingUI extends React.PureComponent {
38973
39470
  })));
38974
39471
  }
38975
39472
  }
39473
+ HighlightingUI.contextType = PerseusI18nContext;
38976
39474
 
38977
39475
  /**
38978
39476
  * This component, whenever its content updates, traverses the DOM to find
@@ -39263,16 +39761,30 @@ class RefStart extends React.Component {
39263
39761
  render() {
39264
39762
  return /*#__PURE__*/React.createElement("span", {
39265
39763
  style: REF_STYLE
39266
- }, i18n.doNotTranslate("_"));
39764
+ }, "_");
39267
39765
  }
39268
39766
  }
39269
39767
  class RefEnd extends React.Component {
39270
39768
  render() {
39271
39769
  return /*#__PURE__*/React.createElement("span", {
39272
39770
  style: REF_STYLE
39273
- }, i18n.doNotTranslate("_"));
39771
+ }, "_");
39274
39772
  }
39275
39773
  }
39774
+ const AltText = ({
39775
+ id,
39776
+ number
39777
+ }) => {
39778
+ const {
39779
+ strings
39780
+ } = usePerseusI18n();
39781
+ return /*#__PURE__*/React.createElement("span", {
39782
+ key: "alt-text",
39783
+ className: "perseus-sr-only"
39784
+ }, strings[id]({
39785
+ number
39786
+ }));
39787
+ };
39276
39788
  const rules = {
39277
39789
  newline: SimpleMarkdown.defaultRules.newline,
39278
39790
  paragraph: SimpleMarkdown.defaultRules.paragraph,
@@ -39441,12 +39953,10 @@ const rules = {
39441
39953
  "aria-hidden": "true"
39442
39954
  }, /*#__PURE__*/React.createElement("span", {
39443
39955
  style: SQUARE_LABEL_STYLE
39444
- }, node.content)), /*#__PURE__*/React.createElement("span", {
39445
- key: "alt-text",
39446
- className: "perseus-sr-only"
39447
- }, i18n.$_("[Marker for question %(number)s]", {
39956
+ }, node.content)), /*#__PURE__*/React.createElement(AltText, {
39957
+ id: "questionMarker",
39448
39958
  number: node.content
39449
- })), node.space ? "\u00A0" : null];
39959
+ }), node.space ? "\u00A0" : null];
39450
39960
  }
39451
39961
  },
39452
39962
  circleLabel: {
@@ -39467,12 +39977,10 @@ const rules = {
39467
39977
  "aria-hidden": true
39468
39978
  }, /*#__PURE__*/React.createElement("span", {
39469
39979
  style: CIRCLE_LABEL_STYLE
39470
- }, node.content)), /*#__PURE__*/React.createElement("span", {
39471
- key: "alt-text",
39472
- className: "perseus-sr-only"
39473
- }, i18n.$_("[Circle marker %(number)s]", {
39980
+ }, node.content)), /*#__PURE__*/React.createElement(AltText, {
39981
+ id: "circleMarker",
39474
39982
  number: node.content
39475
- })), node.space ? "\u00A0" : null];
39983
+ }), node.space ? "\u00A0" : null];
39476
39984
  }
39477
39985
  },
39478
39986
  squareBracketRef: {
@@ -39492,12 +40000,10 @@ const rules = {
39492
40000
  key: "visual-brackets",
39493
40001
  className: "perseus-passage-bracket-label",
39494
40002
  "aria-hidden": "true"
39495
- }, "[", node.content, "]"), /*#__PURE__*/React.createElement("span", {
39496
- key: "alt-text",
39497
- className: "perseus-sr-only"
39498
- }, i18n.$_("[Sentence %(number)s]", {
40003
+ }, "[", node.content, "]"), /*#__PURE__*/React.createElement(AltText, {
40004
+ id: "sentenceMarker",
39499
40005
  number: node.content
39500
- })), node.space ? "\u00A0" : null];
40006
+ }), node.space ? "\u00A0" : null];
39501
40007
  }
39502
40008
  },
39503
40009
  highlight: {
@@ -39888,15 +40394,18 @@ class Passage extends React.Component {
39888
40394
  _renderInstructions(parseState) {
39889
40395
  const firstQuestionNumber = parseState.firstQuestionRef;
39890
40396
  const firstSentenceRef = parseState.firstSentenceRef;
40397
+ const {
40398
+ strings
40399
+ } = this.context;
39891
40400
  let instructions = "";
39892
40401
  if (firstQuestionNumber) {
39893
- instructions += i18n._("The symbol %(questionSymbol)s indicates that question " + "%(questionNumber)s references this portion of the " + "passage.", {
40402
+ instructions += strings.symbolPassage({
39894
40403
  questionSymbol: "[[" + firstQuestionNumber + "]]",
39895
40404
  questionNumber: firstQuestionNumber
39896
40405
  });
39897
40406
  }
39898
40407
  if (firstSentenceRef) {
39899
- instructions += i18n._(" The symbol %(sentenceSymbol)s indicates that the " + "following sentence is referenced in a question.", {
40408
+ instructions += strings.symbolQuestion({
39900
40409
  sentenceSymbol: "[" + firstSentenceRef + "]"
39901
40410
  });
39902
40411
  }
@@ -39942,6 +40451,9 @@ class Passage extends React.Component {
39942
40451
  return PassageMarkdown.output(parsed);
39943
40452
  }
39944
40453
  render() {
40454
+ const {
40455
+ strings
40456
+ } = this.context;
39945
40457
  let lineNumbers;
39946
40458
  const nLines = this.state.nLines;
39947
40459
  if (this.props.showLineNumbers && nLines) {
@@ -39950,9 +40462,7 @@ class Passage extends React.Component {
39950
40462
  lineNumbers = _.range(1, nLines + 1).map(lineN => {
39951
40463
  const lineAt = lineN + this.state.startLineNumbersAfter;
39952
40464
  if (lineAt === 4) {
39953
- // we leave a space in i18n to disambiguate translations, see TP-5392
39954
- // I18N: a label next to a reading passage to denote the line number
39955
- const translatedLine = i18n._("Line ");
40465
+ const translatedLine = strings.lineLabel;
39956
40466
  return /*#__PURE__*/React.createElement("span", {
39957
40467
  key: "line-marker",
39958
40468
  className: "line-marker"
@@ -39982,13 +40492,14 @@ class Passage extends React.Component {
39982
40492
  className: "passage-title"
39983
40493
  }, /*#__PURE__*/React.createElement(Renderer, {
39984
40494
  content: this.props.passageTitle,
39985
- linterContext: this.props.linterContext
40495
+ linterContext: this.props.linterContext,
40496
+ strings: strings
39986
40497
  })), lineNumbers && /*#__PURE__*/React.createElement("div", {
39987
40498
  className: "line-numbers",
39988
40499
  "aria-hidden": true
39989
40500
  }, lineNumbers), !hasTitle && /*#__PURE__*/React.createElement("h3", {
39990
40501
  className: "perseus-sr-only"
39991
- }, i18n._("Beginning of reading passage.")), /*#__PURE__*/React.createElement("div", {
40502
+ }, strings.beginningPassage), /*#__PURE__*/React.createElement("div", {
39992
40503
  className: "passage-text"
39993
40504
  }, this.shouldRenderJipt() ?
39994
40505
  /*#__PURE__*/
@@ -39996,18 +40507,20 @@ class Passage extends React.Component {
39996
40507
  // content to a <Renderer /> which knows how
39997
40508
  // to handle rendering JIPT text.
39998
40509
  React.createElement(Renderer, {
39999
- content: this.props.passageText
40510
+ content: this.props.passageText,
40511
+ strings: strings
40000
40512
  }) : this._renderContent(parsedContent)), this._hasFootnotes() && [/*#__PURE__*/React.createElement("h4", {
40001
40513
  key: "footnote-start",
40002
40514
  className: "perseus-sr-only"
40003
- }, i18n._("Beginning of reading passage footnotes.")), /*#__PURE__*/React.createElement("div", {
40515
+ }, strings.beginningFootnotes), /*#__PURE__*/React.createElement("div", {
40004
40516
  key: "footnotes",
40005
40517
  className: "footnotes"
40006
40518
  }, this._renderFootnotes())], /*#__PURE__*/React.createElement("div", {
40007
40519
  className: "perseus-sr-only"
40008
- }, i18n._("End of reading passage.")))));
40520
+ }, strings.endPassage))));
40009
40521
  }
40010
40522
  }
40523
+ Passage.contextType = PerseusI18nContext;
40011
40524
  Passage.defaultProps = {
40012
40525
  passageTitle: "",
40013
40526
  passageText: "",
@@ -40058,10 +40571,12 @@ class PassageRefTarget extends React.Component {
40058
40571
  // inline={true}
40059
40572
  ,
40060
40573
  apiOptions: this.props.apiOptions,
40061
- linterContext: this.props.linterContext
40574
+ linterContext: this.props.linterContext,
40575
+ strings: this.context.strings
40062
40576
  });
40063
40577
  }
40064
40578
  }
40579
+ PassageRefTarget.contextType = PerseusI18nContext;
40065
40580
  PassageRefTarget.defaultProps = {
40066
40581
  content: "",
40067
40582
  linterContext: linterContextDefault
@@ -40245,11 +40760,11 @@ class Plotter extends React.Component {
40245
40760
  }
40246
40761
  }
40247
40762
  if ((isBar || isLine) && isMobile && !this.props.static) {
40248
- 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");
40763
+ 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");
40249
40764
  }
40250
40765
  self.setupCategories(config);
40251
40766
  if (isTiledPlot && isMobile) {
40252
- 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");
40767
+ 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");
40253
40768
  }
40254
40769
  if (isTiledPlot) {
40255
40770
  self.drawPicHeights(self.state.values, prevState.values);
@@ -40864,6 +41379,7 @@ class Plotter extends React.Component {
40864
41379
  });
40865
41380
  }
40866
41381
  }
41382
+ Plotter.contextType = PerseusI18nContext;
40867
41383
  Plotter.defaultProps = {
40868
41384
  type: "bar",
40869
41385
  labels: ["", ""],
@@ -41135,7 +41651,7 @@ class Table extends React.Component {
41135
41651
  };
41136
41652
  this.simpleValidate = rubric => {
41137
41653
  // @ts-expect-error - TS2339 - Property 'validate' does not exist on type 'typeof Table'.
41138
- return Table.validate(this.getUserInput(), rubric);
41654
+ return Table.validate(this.getUserInput(), rubric, this.context.strings);
41139
41655
  };
41140
41656
  this._handleFocus = inputPath => {
41141
41657
  this.props.onFocus(inputPath);
@@ -41240,7 +41756,8 @@ class Table extends React.Component {
41240
41756
  key: i
41241
41757
  }, /*#__PURE__*/React.createElement(Renderer, {
41242
41758
  content: header,
41243
- linterContext: this.props.linterContext
41759
+ linterContext: this.props.linterContext,
41760
+ strings: this.context.strings
41244
41761
  }));
41245
41762
  }))), /*#__PURE__*/React.createElement("tbody", null, _(rows).times(r => {
41246
41763
  return /*#__PURE__*/React.createElement("tr", {
@@ -41262,6 +41779,7 @@ class Table extends React.Component {
41262
41779
  })));
41263
41780
  }
41264
41781
  }
41782
+ Table.contextType = PerseusI18nContext;
41265
41783
  Table.propTypes = {
41266
41784
  answers: PropTypes.arrayOf(PropTypes.arrayOf(PropTypes.string)),
41267
41785
  editableHeaders: PropTypes.bool,
@@ -41289,7 +41807,7 @@ Table.defaultProps = function () {
41289
41807
  };
41290
41808
  }();
41291
41809
  _.extend(Table, {
41292
- validate: function (state, rubric) {
41810
+ validate: function (state, rubric, strings) {
41293
41811
  const filterNonEmpty = function filterNonEmpty(table) {
41294
41812
  return _.filter(table, function (row) {
41295
41813
  // Check if row has a cell that is nonempty
@@ -41327,7 +41845,7 @@ _.extend(Table, {
41327
41845
  const cellSolution = rowSolution[i];
41328
41846
  const validator = createValidator(cellSolution, {
41329
41847
  simplify: true
41330
- });
41848
+ }, strings);
41331
41849
  const result = validator(cellSupplied);
41332
41850
  if (result.message) {
41333
41851
  // @ts-expect-error - TS2322 - Type 'string' is not assignable to type 'null'.
@@ -42090,7 +42608,7 @@ class OldUnitInput extends React.Component {
42090
42608
  });
42091
42609
  };
42092
42610
  this.simpleValidate = (rubric, onInputError) => {
42093
- return OldUnitInput.validate(this.getUserInput(), rubric);
42611
+ return OldUnitInput.validate(this.getUserInput(), rubric, this.context.strings);
42094
42612
  };
42095
42613
  this.getUserInput = () => {
42096
42614
  return this.props.value;
@@ -42143,13 +42661,13 @@ class OldUnitInput extends React.Component {
42143
42661
  return "unit";
42144
42662
  };
42145
42663
  }
42146
- static validate(userInput, rubric) {
42664
+ static validate(userInput, rubric, strings) {
42147
42665
  const answer = KAS.unitParse(rubric.value).expr;
42148
42666
  const guess = KAS.unitParse(userInput);
42149
42667
  if (!guess.parsed) {
42150
42668
  return {
42151
42669
  type: "invalid",
42152
- message: i18n._("I couldn't understand those units.")
42670
+ message: strings.dontUnderstandUnits
42153
42671
  };
42154
42672
  }
42155
42673
 
@@ -42164,8 +42682,7 @@ class OldUnitInput extends React.Component {
42164
42682
  const sigfigs = rubric.sigfigs;
42165
42683
  const sigfigsCorrect = countSigfigs(guess.coefficient) === sigfigs;
42166
42684
  if (!sigfigsCorrect) {
42167
- // @ts-expect-error - TS2322 - Type 'string' is not assignable to type 'null'.
42168
- message = i18n._("Check your significant figures.");
42685
+ message = strings.checkSigFigs;
42169
42686
  }
42170
42687
 
42171
42688
  // now we need to check that the answer is correct to the precision we
@@ -42184,8 +42701,7 @@ class OldUnitInput extends React.Component {
42184
42701
  numericallyCorrect = false;
42185
42702
  }
42186
42703
  if (!numericallyCorrect) {
42187
- // @ts-expect-error - TS2322 - Type 'string' is not assignable to type 'null'.
42188
- message = i18n._("That answer is numerically incorrect.");
42704
+ message = strings.answerNumericallyIncorrect;
42189
42705
  }
42190
42706
  let kasCorrect;
42191
42707
  const guessUnit = primUnits(guess.expr.simplify());
@@ -42205,8 +42721,7 @@ class OldUnitInput extends React.Component {
42205
42721
  });
42206
42722
  }
42207
42723
  if (!kasCorrect) {
42208
- // @ts-expect-error - TS2322 - Type 'string' is not assignable to type 'null'.
42209
- message = i18n._("Check your units.");
42724
+ message = strings.checkUnits;
42210
42725
  }
42211
42726
  const correct = kasCorrect && numericallyCorrect && sigfigsCorrect;
42212
42727
  return {
@@ -42255,12 +42770,13 @@ class OldUnitInput extends React.Component {
42255
42770
  style: {
42256
42771
  display: "none"
42257
42772
  }
42258
- }, i18n._("I don't understand that")));
42773
+ }, this.context.strings.dontUnderstand));
42259
42774
  }
42260
42775
  }
42261
42776
 
42262
42777
  // Extract the primitive units from a unit expression. This first simplifies
42263
42778
  // `expr` to a `Mul` like "5 kg m / s^2" then removes the first term.
42779
+ OldUnitInput.contextType = PerseusI18nContext;
42264
42780
  OldUnitInput.defaultProps = {
42265
42781
  apiOptions: ApiOptions.defaults,
42266
42782
  value: ""
@@ -42311,9 +42827,12 @@ const VideoTranscriptLink = props => {
42311
42827
  // which is used by our GraphQL framework.
42312
42828
  // @ts-expect-error - TS2345 - Argument of type 'string' is not assignable to parameter of type 'VideoKind'.
42313
42829
  const result = useVideo(id, kind);
42830
+ const {
42831
+ strings
42832
+ } = usePerseusI18n();
42314
42833
  switch (result.status) {
42315
42834
  case "loading":
42316
- return /*#__PURE__*/React.createElement(View, null, i18n._("Loading..."));
42835
+ return /*#__PURE__*/React.createElement(View, null, strings.loading);
42317
42836
  case "success":
42318
42837
  {
42319
42838
  var _result$data;
@@ -42326,14 +42845,14 @@ const VideoTranscriptLink = props => {
42326
42845
  href: "/transcript/" + ((video == null ? void 0 : video.contentId) || "videoNotFound"),
42327
42846
  target: "_blank",
42328
42847
  className: "visited-no-recolor"
42329
- }, i18n._("See video transcript")));
42848
+ }, strings.videoTranscript));
42330
42849
  }
42331
42850
  case "error":
42332
- return /*#__PURE__*/React.createElement(View, null, i18n._("Something went wrong."));
42851
+ return /*#__PURE__*/React.createElement(View, null, strings.somethingWrong);
42333
42852
  case "aborted":
42334
- return /*#__PURE__*/React.createElement(View, null, i18n._("Something went wrong."));
42853
+ return /*#__PURE__*/React.createElement(View, null, strings.somethingWrong);
42335
42854
  default:
42336
- return /*#__PURE__*/React.createElement(View, null, i18n._("Something went wrong."));
42855
+ return /*#__PURE__*/React.createElement(View, null, strings.somethingWrong);
42337
42856
  }
42338
42857
  };
42339
42858
  const styles$4 = StyleSheet.create({
@@ -42425,7 +42944,7 @@ class Video extends React.Component {
42425
42944
  key: location + this.props.alignment
42426
42945
  }, /*#__PURE__*/React.createElement(View, {
42427
42946
  style: a11y.srOnly
42428
- }, i18n._("Khan Academy video wrapper")), /*#__PURE__*/React.createElement("iframe", {
42947
+ }, this.context.strings.videoWrapper), /*#__PURE__*/React.createElement("iframe", {
42429
42948
  // TODO(joshuan): Consider not using iframes when we're
42430
42949
  // loading this from webapp. This iframe is problematic
42431
42950
  // for screenreaders.
@@ -42440,6 +42959,7 @@ class Video extends React.Component {
42440
42959
  }));
42441
42960
  }
42442
42961
  }
42962
+ Video.contextType = PerseusI18nContext;
42443
42963
  var Video$1 = {
42444
42964
  name: "video",
42445
42965
  displayName: "Video",
@@ -42489,7 +43009,7 @@ const init = function init(options) {
42489
43009
 
42490
43010
  // This file is processed by a Rollup plugin (replace) to inject the production
42491
43011
  const libName = "@khanacademy/perseus";
42492
- const libVersion = "21.5.0";
43012
+ const libVersion = "22.0.0";
42493
43013
  addLibraryVersionToPerseusDebug(libName, libVersion);
42494
43014
 
42495
43015
  const apiVersion = {
@@ -42655,8 +43175,9 @@ class ArticleRenderer extends React.Component {
42655
43175
  }
42656
43176
  }),
42657
43177
  linterContext: PerseusLinter.pushContextStack(this.props.linterContext, "article"),
42658
- legacyPerseusLint: this.props.legacyPerseusLint
42659
- }, this.props.dependencies)));
43178
+ legacyPerseusLint: this.props.legacyPerseusLint,
43179
+ strings: this.context.strings
43180
+ })));
42660
43181
  });
42661
43182
  return /*#__PURE__*/React.createElement("div", {
42662
43183
  className: classes
@@ -42665,6 +43186,7 @@ class ArticleRenderer extends React.Component {
42665
43186
  }, sections));
42666
43187
  }
42667
43188
  }
43189
+ ArticleRenderer.contextType = PerseusI18nContext;
42668
43190
  ArticleRenderer.defaultProps = {
42669
43191
  apiOptions: ApiOptions.defaults,
42670
43192
  useNewStyles: false,
@@ -42717,7 +43239,7 @@ class HintRenderer extends React.Component {
42717
43239
  tabIndex: "-1"
42718
43240
  }, !apiOptions.isMobile && /*#__PURE__*/React.createElement("span", {
42719
43241
  className: "perseus-sr-only"
42720
- }, i18n._("Hint #%(pos)s", {
43242
+ }, this.context.strings.hintPos({
42721
43243
  pos: pos + 1
42722
43244
  })), !apiOptions.isMobile && totalHints != null && pos != null && /*#__PURE__*/React.createElement("span", {
42723
43245
  className: "perseus-hint-label",
@@ -42734,11 +43256,13 @@ class HintRenderer extends React.Component {
42734
43256
  images: hint.images,
42735
43257
  apiOptions: rendererApiOptions,
42736
43258
  findExternalWidgets: this.props.findExternalWidgets,
42737
- linterContext: PerseusLinter.pushContextStack(this.props.linterContext, "hint")
43259
+ linterContext: PerseusLinter.pushContextStack(this.props.linterContext, "hint"),
43260
+ strings: this.context.strings
42738
43261
  }))
42739
43262
  );
42740
43263
  }
42741
43264
  }
43265
+ HintRenderer.contextType = PerseusI18nContext;
42742
43266
  HintRenderer.defaultProps = {
42743
43267
  linterContext: PerseusLinter.linterContextDefault
42744
43268
  };
@@ -42920,7 +43444,7 @@ class HintsRenderer extends React.Component {
42920
43444
  className: classNames$1
42921
43445
  }, apiOptions.isMobile && hintsVisible > 0 && /*#__PURE__*/React.createElement("div", {
42922
43446
  className: css(styles$2.mobileHintStylesHintTitle, sharedStyles.responsiveLabel)
42923
- }, i18n._("Hints")), hints, showGetAnotherHint && /*#__PURE__*/React.createElement("button", {
43447
+ }, this.context.strings.hints), hints, showGetAnotherHint && /*#__PURE__*/React.createElement("button", {
42924
43448
  // @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>'.
42925
43449
  rel: "button",
42926
43450
  className: css(styles$2.linkButton, styles$2.getAnotherHintButton, apiOptions.isMobile && styles$2.mobileHintStylesGetAnotherHintButton),
@@ -42935,9 +43459,10 @@ class HintsRenderer extends React.Component {
42935
43459
  className: css(styles$2.plusText, apiOptions.isMobile && styles$2.mobileHintStylesPlusText)
42936
43460
  }, "+"), /*#__PURE__*/React.createElement("span", {
42937
43461
  className: css(styles$2.getAnotherHintText)
42938
- }, i18n._("Get another hint"), " ", hintRatioCopy)));
43462
+ }, this.context.strings.getAnotherHint, " ", hintRatioCopy)));
42939
43463
  }
42940
43464
  }
43465
+ HintsRenderer.contextType = PerseusI18nContext;
42941
43466
  HintsRenderer.defaultProps = {
42942
43467
  linterContext: PerseusLinter.linterContextDefault
42943
43468
  };
@@ -43328,14 +43853,16 @@ class ServerItemRenderer extends React.Component {
43328
43853
  content: this.props.item.question.content,
43329
43854
  widgets: this.props.item.question.widgets,
43330
43855
  images: this.props.item.question.images,
43331
- linterContext: PerseusLinter.pushContextStack(this.props.linterContext, "question")
43856
+ linterContext: PerseusLinter.pushContextStack(this.props.linterContext, "question"),
43857
+ strings: this.context.strings
43332
43858
  }, this.props.dependencies)));
43333
43859
  const hintsRenderer = /*#__PURE__*/React.createElement(HintsRenderer, {
43334
43860
  hints: this.props.item.hints,
43335
43861
  hintsVisible: this.props.hintsVisible,
43336
43862
  apiOptions: apiOptions,
43337
43863
  ref: elem => this.hintsRenderer = elem,
43338
- linterContext: PerseusLinter.pushContextStack(this.props.linterContext, "hints")
43864
+ linterContext: PerseusLinter.pushContextStack(this.props.linterContext, "hints"),
43865
+ strings: this.context.strings
43339
43866
  });
43340
43867
  return /*#__PURE__*/React.createElement(DependenciesContext.Provider, {
43341
43868
  value: this.props.dependencies
@@ -43349,6 +43876,7 @@ class ServerItemRenderer extends React.Component {
43349
43876
  }, hintsRenderer)));
43350
43877
  }
43351
43878
  }
43879
+ ServerItemRenderer.contextType = PerseusI18nContext;
43352
43880
  ServerItemRenderer.defaultProps = {
43353
43881
  apiOptions: {},
43354
43882
  // a deep default is done in `this.update()`
@@ -43511,114 +44039,6 @@ var constants = /*#__PURE__*/Object.freeze({
43511
44039
  perseusFrameBorderWidth: perseusFrameBorderWidth
43512
44040
  });
43513
44041
 
43514
- // TODO(FEI-4010): Remove `Perseus` prefix for all types here
43515
- // TODO(FEI-4011): Use types generated by https://github.com/jaredly/generate-perseus-flowtypes
43516
-
43517
- // Range is replaced within this file with Interval, but it is used elsewhere
43518
- // and exported from the package, so we need to keep it around.
43519
- // TODO(FEI-5054): Figure out how to get global .d.ts files working with monorepos
43520
- const ItemExtras = [
43521
- // The user might benefit from using a Scientific Calculator. Provided on Khan Academy when true
43522
- "calculator",
43523
- // The user might benefit from using a statistics Chi Squared Table like https://people.richland.edu/james/lecture/m170/tbl-chi.html
43524
- "chi2Table",
43525
- // The user might benefit from a monthly payments calculator. Provided on Khan Academy when true
43526
- "financialCalculatorMonthlyPayment",
43527
- // The user might benefit from a total amount calculator. Provided on Khan Academy when true
43528
- "financialCalculatorTotalAmount",
43529
- // The user might benefit from a time to pay off calculator. Provided on Khan Academy when true
43530
- "financialCalculatorTimeToPayOff",
43531
- // The user might benefit from using a Periodic Table of Elements. Provided on Khan Academy when true
43532
- "periodicTable",
43533
- // The user might benefit from using a Periodic Table of Elements with key. Provided on Khan Academy when true
43534
- "periodicTableWithKey",
43535
- // The user might benefit from using a statistics T Table like https://www.statisticshowto.com/tables/t-distribution-table/
43536
- "tTable",
43537
- // The user might benefit from using a statistics Z Table like https://www.ztable.net/
43538
- "zTable"];
43539
-
43540
- // prettier-ignore
43541
-
43542
- // prettier-ignore
43543
-
43544
- // prettier-ignore
43545
-
43546
- // prettier-ignore
43547
-
43548
- // prettier-ignore
43549
-
43550
- // prettier-ignore
43551
-
43552
- // prettier-ignore
43553
-
43554
- // prettier-ignore
43555
-
43556
- // prettier-ignore
43557
-
43558
- // prettier-ignore
43559
-
43560
- // prettier-ignore
43561
-
43562
- // prettier-ignore
43563
-
43564
- // prettier-ignore
43565
-
43566
- // prettier-ignore
43567
-
43568
- // prettier-ignore
43569
-
43570
- // prettier-ignore
43571
-
43572
- // prettier-ignore
43573
-
43574
- // prettier-ignore
43575
-
43576
- // prettier-ignore
43577
-
43578
- // prettier-ignore
43579
-
43580
- // prettier-ignore
43581
-
43582
- // prettier-ignore
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
- // A background image applied to various widgets.
43615
-
43616
- const PerseusExpressionAnswerFormConsidered = ["correct", "wrong", "ungraded"];
43617
-
43618
- // 2D range: xMin, xMax, yMin, yMax
43619
-
43620
- const plotterPlotTypes = ["bar", "line", "pic", "histogram", "dotplot"];
43621
-
43622
44042
  /**
43623
44043
  * Traverses a {content, widgets, images} renderer props object,
43624
44044
  * such as `itemData.question`
@@ -44227,7 +44647,9 @@ class MultiRenderer extends React.Component {
44227
44647
  // `<Renderer>`. I'm not sure how else to do this.
44228
44648
  const _this$props = this.props,
44229
44649
  otherProps = _objectWithoutPropertiesLoose(_this$props, _excluded);
44230
- return otherProps;
44650
+ return _extends({}, otherProps, {
44651
+ strings: this.context.strings
44652
+ });
44231
44653
  }
44232
44654
 
44233
44655
  /**
@@ -44478,7 +44900,7 @@ class MultiRenderer extends React.Component {
44478
44900
  if (this.state.renderError) {
44479
44901
  return /*#__PURE__*/React.createElement("div", {
44480
44902
  className: css(styles.error)
44481
- }, i18n.$_("Error rendering: %(error)s", {
44903
+ }, this.context.strings.errorRendering({
44482
44904
  error: String(this.state.renderError)
44483
44905
  }));
44484
44906
  }
@@ -44492,6 +44914,7 @@ class MultiRenderer extends React.Component {
44492
44914
  }));
44493
44915
  }
44494
44916
  }
44917
+ MultiRenderer.contextType = PerseusI18nContext;
44495
44918
  const styles = StyleSheet.create({
44496
44919
  error: {
44497
44920
  color: "red"
@@ -45587,5 +46010,5 @@ const EditorJsonify = {
45587
46010
  }
45588
46011
  };
45589
46012
 
45590
- 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, mediaQueries, parsePerseusItem, plotterPlotTypes, preprocessTex, registerAllWidgetsForTesting, shapes, shouldHaveIndividualAnswer, traverse, violatingWidgets, allWidgets as widgets };
46013
+ 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 };
45591
46014
  //# sourceMappingURL=index.js.map