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", () => {