From 96f0337ce459dea6a0860b45704e188876d38720 Mon Sep 17 00:00:00 2001 From: Matthew Date: Thu, 22 Aug 2024 09:54:22 -0500 Subject: [PATCH] Update Mathquill Mathfield with Portuguese trig functions (#1538) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary: So basically: - These two PRs ([1](https://github.com/Khan/perseus/pull/921) and [2](https://github.com/Khan/webapp/pull/19111)) helped add support for Portuguese trig functions when typed in the input. - This [PR](https://github.com/Khan/perseus/pull/1497) added support for the text of trig buttons to be translated, however the input would still update with "sin" if the "sen" button was pressed. - The PR being reviewed updates the input correctly; so if the "sen" button is pressed, the input is updated with "sen" How this works: - When a button is pressed, the keypad yells into the void that "SIN" was pressed - The keypad translator has a callback for updating the input when "SIN" is pressed - BEFORE: it would just updated the input with `sin()` - NOW: it looks for the translated version of "sin", checks it against known safe translations, and either updates it with a known safe translation or the default "sin" Issue: LEMS-1621 ## Test plan: Once the strings are translated: - Go to a Portuguese page with an expression widget - Go to a question that requires the "sin" button - Open the keypad - Note the button is "sen" instead of "sin" - Click the "sen" button - Note the input is updated with "sen()" - Fill out the correct answer and submit - Note the validator accepts "sen" instead of "sin" Author: handeyeco Reviewers: handeyeco, benchristel, anakaren-rojas Required Reviewers: Approved By: benchristel Checks: ✅ codecov/project, ✅ codecov/patch, ✅ Upload Coverage (ubuntu-latest, 20.x), ✅ Publish npm snapshot (ubuntu-latest, 20.x), ✅ Check builds for changes in size (ubuntu-latest, 20.x), ✅ Cypress (ubuntu-latest, 20.x), ✅ Jest Coverage (ubuntu-latest, 20.x), ✅ Check for .changeset entries for all changed files (ubuntu-latest, 20.x), ✅ Lint, Typecheck, Format, and Test (ubuntu-latest, 20.x), ✅ Publish Storybook to Chromatic (ubuntu-latest, 20.x), ✅ gerald Pull Request URL: https://github.com/Khan/perseus/pull/1538 --- .changeset/orange-rules-knock.md | 6 ++ .../src/components/input/math-wrapper.ts | 2 +- .../components/key-handlers/key-translator.ts | 38 ++++++- .../__tests__/keypad-v2-mathquill.test.tsx | 101 +++++++++++++++++- .../keypad/keypad-mathquill.stories.tsx | 11 +- .../keypad/keypad-pages/geometry-page.tsx | 1 + .../perseus/src/components/math-input.tsx | 4 +- packages/perseus/src/strings.ts | 9 ++ .../src/widgets/__tests__/expression.test.tsx | 10 ++ 9 files changed, 170 insertions(+), 12 deletions(-) create mode 100644 .changeset/orange-rules-knock.md diff --git a/.changeset/orange-rules-knock.md b/.changeset/orange-rules-knock.md new file mode 100644 index 0000000000..a3951f9882 --- /dev/null +++ b/.changeset/orange-rules-knock.md @@ -0,0 +1,6 @@ +--- +"@khanacademy/math-input": patch +"@khanacademy/perseus": patch +--- + +Use Portuguese sen and tg when updating Mathquill from the keypad diff --git a/packages/math-input/src/components/input/math-wrapper.ts b/packages/math-input/src/components/input/math-wrapper.ts index e42b54e35a..5b09985b18 100644 --- a/packages/math-input/src/components/input/math-wrapper.ts +++ b/packages/math-input/src/components/input/math-wrapper.ts @@ -63,7 +63,7 @@ class MathWrapper { this.callbacks = callbacks; this.mobileKeyTranslator = { - ...getKeyTranslator(locale), + ...getKeyTranslator(locale, strings), // note(Matthew): our mobile backspace logic is really complicated // and for some reason doesn't really work in the desktop experience. // So we default to the basic backspace functionality in the diff --git a/packages/math-input/src/components/key-handlers/key-translator.ts b/packages/math-input/src/components/key-handlers/key-translator.ts index adf4ea762e..37eb8deabe 100644 --- a/packages/math-input/src/components/key-handlers/key-translator.ts +++ b/packages/math-input/src/components/key-handlers/key-translator.ts @@ -40,6 +40,30 @@ function buildGenericCallback( }; } +/** + * This lets us use translated functions + * (like tg->tan and sen->sin) when we know it's safe to. + * This lets us progressively support translations without needing + * to support every language all at once. + * + * @param {string} command - the translated command/function to check + * @param {string[]} supportedTranslations - list of translations we support + * @param {string} defaultCommand - what to fallback to if the command isn't supported + */ +function buildTranslatableFunctionCallback( + command: string, + supportedTranslations: string[], + defaultCommand: string, +) { + const cmd = supportedTranslations.includes(command) + ? command + : defaultCommand; + return function (mathField: MathFieldInterface) { + mathField.write(`${cmd}\\left(\\right)`); + mathField.keystroke("Left"); + }; +} + function buildNormalFunctionCallback(command: string) { return function (mathField: MathFieldInterface) { mathField.write(`\\${command}\\left(\\right)`); @@ -47,8 +71,15 @@ function buildNormalFunctionCallback(command: string) { }; } +type KeyTranslatorStrings = { + sin: string; + cos: string; + tan: string; +}; + export const getKeyTranslator = ( locale: string, + strings: KeyTranslatorStrings, ): Record => ({ EXP: handleExponent, EXP_2: handleExponent, @@ -66,9 +97,10 @@ export const getKeyTranslator = ( LOG: buildNormalFunctionCallback("log"), LN: buildNormalFunctionCallback("ln"), - SIN: buildNormalFunctionCallback("sin"), - COS: buildNormalFunctionCallback("cos"), - TAN: buildNormalFunctionCallback("tan"), + + COS: buildNormalFunctionCallback(strings.cos), + SIN: buildTranslatableFunctionCallback(strings.sin, ["sin", "sen"], "sin"), + TAN: buildTranslatableFunctionCallback(strings.tan, ["tan", "tg"], "tan"), CDOT: buildGenericCallback("\\cdot"), DECIMAL: buildGenericCallback(getDecimalSeparator(locale)), diff --git a/packages/math-input/src/components/keypad/__tests__/keypad-v2-mathquill.test.tsx b/packages/math-input/src/components/keypad/__tests__/keypad-v2-mathquill.test.tsx index 2a060c5c34..b55ba70046 100644 --- a/packages/math-input/src/components/keypad/__tests__/keypad-v2-mathquill.test.tsx +++ b/packages/math-input/src/components/keypad/__tests__/keypad-v2-mathquill.test.tsx @@ -18,6 +18,7 @@ type Props = { onChangeMathInput: (mathInputTex: string) => void; keypadClosed?: boolean; onAnalyticsEvent?: AnalyticsEventHandlerFn; + portuguese?: boolean; }; function V2KeypadWithMathquill(props: Props) { @@ -27,6 +28,14 @@ function V2KeypadWithMathquill(props: Props) { const [keypadOpen, setKeypadOpen] = React.useState(!keypadClosed); const {strings} = useMathInputI18n(); + if (props.portuguese) { + strings.sin = "sen"; + strings.tan = "tg"; + } else { + strings.sin = "sin"; + strings.tan = "tan"; + } + React.useEffect(() => { if (!mathField && mathFieldWrapperRef.current) { const mathFieldInstance = createMathField( @@ -48,7 +57,7 @@ function V2KeypadWithMathquill(props: Props) { } }, [mathField, strings, onChangeMathInput]); - const keyTranslator = getKeyTranslator("en"); + const keyTranslator = getKeyTranslator("en", strings); function handleClickKey(key: Key) { if (!mathField) { @@ -325,4 +334,94 @@ describe("Keypad v2 with MathQuill", () => { payload: {virtualKeypadVersion: "MATH_INPUT_KEYPAD_V2"}, }); }); + + it("handles english sin trig function", async () => { + // Arrange + const mockMathInputCallback = jest.fn(); + render( + , + ); + + // Act + await userEvent.click(screen.getByRole("tab", {name: "Geometry"})); + await userEvent.click(screen.getByRole("button", {name: "Sine"})); + await userEvent.click(screen.getByRole("tab", {name: "Numbers"})); + await userEvent.click(screen.getByRole("button", {name: "4"})); + await userEvent.click(screen.getByRole("button", {name: "2"})); + + // Assert + expect(mockMathInputCallback).toHaveBeenLastCalledWith( + "\\sin\\left(42\\right)", + ); + }); + + it("handles portuguese sen trig function", async () => { + // Arrange + const mockMathInputCallback = jest.fn(); + render( + , + ); + + // Act + await userEvent.click(screen.getByRole("tab", {name: "Geometry"})); + // This needs to stay as "getByText" because we're validating translations + // and aria-labels are in English + await userEvent.click(screen.getByText("sen")); + await userEvent.click(screen.getByRole("tab", {name: "Numbers"})); + await userEvent.click(screen.getByRole("button", {name: "4"})); + await userEvent.click(screen.getByRole("button", {name: "2"})); + + // Assert + expect(mockMathInputCallback).toHaveBeenLastCalledWith( + "\\operatorname{sen}\\left(42\\right)", + ); + }); + + it("handles english tan trig function", async () => { + // Arrange + const mockMathInputCallback = jest.fn(); + render( + , + ); + + // Act + await userEvent.click(screen.getByRole("tab", {name: "Geometry"})); + await userEvent.click(screen.getByRole("button", {name: "Tangent"})); + await userEvent.click(screen.getByRole("tab", {name: "Numbers"})); + await userEvent.click(screen.getByRole("button", {name: "4"})); + await userEvent.click(screen.getByRole("button", {name: "2"})); + + // Assert + expect(mockMathInputCallback).toHaveBeenLastCalledWith( + "\\tan\\left(42\\right)", + ); + }); + + it("handles portuguese tg trig function", async () => { + // Arrange + const mockMathInputCallback = jest.fn(); + render( + , + ); + + // Act + await userEvent.click(screen.getByRole("tab", {name: "Geometry"})); + // This needs to stay as "getByText" because we're validating translations + // and aria-labels are in English + await userEvent.click(screen.getByText("tg")); + await userEvent.click(screen.getByRole("tab", {name: "Numbers"})); + await userEvent.click(screen.getByRole("button", {name: "4"})); + await userEvent.click(screen.getByRole("button", {name: "2"})); + + // Assert + expect(mockMathInputCallback).toHaveBeenLastCalledWith( + "\\operatorname{tg}\\left(42\\right)", + ); + }); }); diff --git a/packages/math-input/src/components/keypad/keypad-mathquill.stories.tsx b/packages/math-input/src/components/keypad/keypad-mathquill.stories.tsx index 6bd72e0a66..56861e3c92 100644 --- a/packages/math-input/src/components/keypad/keypad-mathquill.stories.tsx +++ b/packages/math-input/src/components/keypad/keypad-mathquill.stories.tsx @@ -44,7 +44,11 @@ export function V2KeypadWithMathquill() { } }, [mathField]); - const keyTranslator = getKeyTranslator("en"); + const keyTranslator = getKeyTranslator("en", { + sin: "sin", + cos: "cos", + tan: "tan", + }); function handleClickKey(key: Key) { if (!mathField) { @@ -86,10 +90,7 @@ export function V2KeypadWithMathquill() { convertDotToTimes preAlgebra trigonometry - onAnalyticsEvent={async (event) => { - // eslint-disable-next-line no-console - console.log("Send Event:", event); - }} + onAnalyticsEvent={async () => {}} showDismiss /> diff --git a/packages/math-input/src/components/keypad/keypad-pages/geometry-page.tsx b/packages/math-input/src/components/keypad/keypad-pages/geometry-page.tsx index c6c104ed6f..f35178cb1e 100644 --- a/packages/math-input/src/components/keypad/keypad-pages/geometry-page.tsx +++ b/packages/math-input/src/components/keypad/keypad-pages/geometry-page.tsx @@ -14,6 +14,7 @@ export default function GeometryPage(props: Props) { const {onClickKey} = props; const {strings} = useMathInputI18n(); const Keys = KeyConfigs(strings); + return ( <> {/* Row 1 */} diff --git a/packages/perseus/src/components/math-input.tsx b/packages/perseus/src/components/math-input.tsx index 2040672fe9..3ccd9d38db 100644 --- a/packages/perseus/src/components/math-input.tsx +++ b/packages/perseus/src/components/math-input.tsx @@ -132,7 +132,7 @@ class InnerMathInput extends React.Component { const input = this.mathField(); const {locale} = this.context; const customKeyTranslator = { - ...getKeyTranslator(locale), + ...getKeyTranslator(locale, this.context.strings), // If there's something in the input that can become part of a // fraction, typing "/" puts it in the numerator. If not, typing // "/" does nothing. In that case, enter a \frac. @@ -259,7 +259,7 @@ class InnerMathInput extends React.Component { handleKeypadPress: (key: Keys, e: any) => void = (key, e) => { const {locale} = this.context; - const translator = getKeyTranslator(locale)[key]; + const translator = getKeyTranslator(locale, this.context.strings)[key]; const mathField = this.mathField(); if (mathField) { diff --git a/packages/perseus/src/strings.ts b/packages/perseus/src/strings.ts index 41c35574ea..1aacc27d58 100644 --- a/packages/perseus/src/strings.ts +++ b/packages/perseus/src/strings.ts @@ -125,6 +125,9 @@ export type PerseusStrings = { videoWrapper: string; mathInputTitle: string; mathInputDescription: string; + sin: string; + cos: string; + tan: string; }; /** @@ -291,6 +294,9 @@ export const strings: { mathInputTitle: "mathematics keyboard", mathInputDescription: "Use keyboard/mouse to interact with math-based input fields", + sin: "sin", + cos: "cos", + tan: "tan", }; /** @@ -441,4 +447,7 @@ export const mockStrings: PerseusStrings = { mathInputTitle: "mathematics keyboard", mathInputDescription: "Use keyboard/mouse to interact with math-based input fields", + sin: "sin", + cos: "cos", + tan: "tan", }; diff --git a/packages/perseus/src/widgets/__tests__/expression.test.tsx b/packages/perseus/src/widgets/__tests__/expression.test.tsx index 9ab6cdc03f..716661b825 100644 --- a/packages/perseus/src/widgets/__tests__/expression.test.tsx +++ b/packages/perseus/src/widgets/__tests__/expression.test.tsx @@ -238,6 +238,16 @@ describe("Expression Widget", function () { const item = expressionItemWithAnswer("sin(x)"); await assertIncorrect(userEvent, item, "2"); }); + + it("allows portugese sen", async () => { + const item = expressionItemWithAnswer("sin(42)"); + await assertCorrect(userEvent, item, "sen(42)"); + }); + + it("allows portugese tg", async () => { + const item = expressionItemWithAnswer("tan(42)"); + await assertCorrect(userEvent, item, "tg(42)"); + }); }); describe("analytics", () => {