diff --git a/frontend/__tests__/utils/strings.spec.ts b/frontend/__tests__/utils/strings.spec.ts
index 062eefd46e0b..78ee18de2b8c 100644
--- a/frontend/__tests__/utils/strings.spec.ts
+++ b/frontend/__tests__/utils/strings.spec.ts
@@ -270,7 +270,7 @@ describe("string utils", () => {
);
});
- describe("getWordDirection", () => {
+ describe("isWordRightToLeft", () => {
beforeEach(() => {
Strings.clearWordDirectionCache();
});
@@ -321,13 +321,27 @@ describe("string utils", () => {
languageRTL: boolean,
_description: string
) => {
- expect(Strings.getWordDirection(word, languageRTL)).toBe(expected);
+ expect(Strings.isWordRightToLeft(word, languageRTL)).toBe(expected);
}
);
it("should return languageRTL for undefined word", () => {
- expect(Strings.getWordDirection(undefined, false)).toBe(false);
- expect(Strings.getWordDirection(undefined, true)).toBe(true);
+ expect(Strings.isWordRightToLeft(undefined, false)).toBe(false);
+ expect(Strings.isWordRightToLeft(undefined, true)).toBe(true);
+ });
+
+ // testing reverseDirection
+ it("should return true for LTR word with reversed direction", () => {
+ expect(Strings.isWordRightToLeft("hello", false, true)).toBe(true);
+ expect(Strings.isWordRightToLeft("hello", true, true)).toBe(true);
+ });
+ it("should return false for RTL word with reversed direction", () => {
+ expect(Strings.isWordRightToLeft("مرحبا", true, true)).toBe(false);
+ expect(Strings.isWordRightToLeft("مرحبا", false, true)).toBe(false);
+ });
+ it("should return reverse of languageRTL for undefined word with reversed direction", () => {
+ expect(Strings.isWordRightToLeft(undefined, false, true)).toBe(true);
+ expect(Strings.isWordRightToLeft(undefined, true, true)).toBe(false);
});
describe("caching", () => {
@@ -349,7 +363,7 @@ describe("string utils", () => {
it("should use cache for repeated calls", () => {
// First call should cache the result (cache miss)
- const result1 = Strings.getWordDirection("hello", false);
+ const result1 = Strings.isWordRightToLeft("hello", false);
expect(result1).toBe(false);
expect(mapSetSpy).toHaveBeenCalledWith("hello", false);
@@ -358,7 +372,7 @@ describe("string utils", () => {
mapSetSpy.mockClear();
// Second call should use cache (cache hit)
- const result2 = Strings.getWordDirection("hello", false);
+ const result2 = Strings.isWordRightToLeft("hello", false);
expect(result2).toBe(false);
expect(mapGetSpy).toHaveBeenCalledWith("hello");
expect(mapSetSpy).not.toHaveBeenCalled(); // Should not set again
@@ -367,7 +381,7 @@ describe("string utils", () => {
mapGetSpy.mockClear();
mapSetSpy.mockClear();
- const result3 = Strings.getWordDirection("hello", true);
+ const result3 = Strings.isWordRightToLeft("hello", true);
expect(result3).toBe(false); // Still false because "hello" is LTR regardless of language
expect(mapGetSpy).toHaveBeenCalledWith("hello");
expect(mapSetSpy).not.toHaveBeenCalled(); // Should not set again
@@ -375,7 +389,7 @@ describe("string utils", () => {
it("should cache based on core word without punctuation", () => {
// First call should cache the result for core "hello"
- const result1 = Strings.getWordDirection("hello", false);
+ const result1 = Strings.isWordRightToLeft("hello", false);
expect(result1).toBe(false);
expect(mapSetSpy).toHaveBeenCalledWith("hello", false);
@@ -383,7 +397,7 @@ describe("string utils", () => {
mapSetSpy.mockClear();
// These should all use the same cache entry since they have the same core
- const result2 = Strings.getWordDirection("hello!", false);
+ const result2 = Strings.isWordRightToLeft("hello!", false);
expect(result2).toBe(false);
expect(mapGetSpy).toHaveBeenCalledWith("hello");
expect(mapSetSpy).not.toHaveBeenCalled();
@@ -391,7 +405,7 @@ describe("string utils", () => {
mapGetSpy.mockClear();
mapSetSpy.mockClear();
- const result3 = Strings.getWordDirection("!hello", false);
+ const result3 = Strings.isWordRightToLeft("!hello", false);
expect(result3).toBe(false);
expect(mapGetSpy).toHaveBeenCalledWith("hello");
expect(mapSetSpy).not.toHaveBeenCalled();
@@ -399,7 +413,7 @@ describe("string utils", () => {
mapGetSpy.mockClear();
mapSetSpy.mockClear();
- const result4 = Strings.getWordDirection("!hello!", false);
+ const result4 = Strings.isWordRightToLeft("!hello!", false);
expect(result4).toBe(false);
expect(mapGetSpy).toHaveBeenCalledWith("hello");
expect(mapSetSpy).not.toHaveBeenCalled();
@@ -407,7 +421,7 @@ describe("string utils", () => {
it("should handle cache clearing", () => {
// Cache a result
- Strings.getWordDirection("test", false);
+ Strings.isWordRightToLeft("test", false);
expect(mapSetSpy).toHaveBeenCalledWith("test", false);
// Clear cache
@@ -419,14 +433,14 @@ describe("string utils", () => {
mapClearSpy.mockClear();
// Should work normally after cache clear (cache miss again)
- const result = Strings.getWordDirection("test", false);
+ const result = Strings.isWordRightToLeft("test", false);
expect(result).toBe(false);
expect(mapSetSpy).toHaveBeenCalledWith("test", false);
});
it("should demonstrate cache miss vs cache hit behavior", () => {
// Test cache miss - first time seeing this word
- const result1 = Strings.getWordDirection("unique", false);
+ const result1 = Strings.isWordRightToLeft("unique", false);
expect(result1).toBe(false);
expect(mapGetSpy).toHaveBeenCalledWith("unique");
expect(mapSetSpy).toHaveBeenCalledWith("unique", false);
@@ -435,7 +449,7 @@ describe("string utils", () => {
mapSetSpy.mockClear();
// Test cache hit - same word again
- const result2 = Strings.getWordDirection("unique", false);
+ const result2 = Strings.isWordRightToLeft("unique", false);
expect(result2).toBe(false);
expect(mapGetSpy).toHaveBeenCalledWith("unique");
expect(mapSetSpy).not.toHaveBeenCalled(); // No cache set on hit
@@ -444,7 +458,7 @@ describe("string utils", () => {
mapSetSpy.mockClear();
// Test cache miss - different word
- const result3 = Strings.getWordDirection("different", false);
+ const result3 = Strings.isWordRightToLeft("different", false);
expect(result3).toBe(false);
expect(mapGetSpy).toHaveBeenCalledWith("different");
expect(mapSetSpy).toHaveBeenCalledWith("different", false);
diff --git a/frontend/src/html/popups.html b/frontend/src/html/popups.html
index 9c78a4b600a5..6ee8facc89e8 100644
--- a/frontend/src/html/popups.html
+++ b/frontend/src/html/popups.html
@@ -178,7 +178,7 @@
sentry
We use Sentry to track errors and performance issues on our site, as
- well as record annonymized user sessions to help us debug issues and
+ well as record anonymized user sessions to help us debug issues and
improve our product.
Sentry is a crash reporting service that helps us track errors and
crashes on the website. It collects information about your device,
browser, and the error that occurred. Sometimes it might also include
- an annonymized replay of your session. This information is used to
+ an anonymized replay of your session. This information is used to
track down bugs faster and improve our website.
diff --git a/frontend/src/styles/test.scss b/frontend/src/styles/test.scss
index edf6777639d4..72631e36668f 100644
--- a/frontend/src/styles/test.scss
+++ b/frontend/src/styles/test.scss
@@ -307,10 +307,6 @@
&.rightToLeftTest {
//flex-direction: row-reverse; // no need for hacking 😉, CSS fully support right-to-left languages
direction: rtl;
- .word {
- //flex-direction: row-reverse;
- direction: rtl;
- }
}
&.withLigatures {
.word {
@@ -749,10 +745,6 @@
&.rightToLeftTest {
//flex-direction: row-reverse; // no need for hacking 😉, CSS fully support right-to-left languages
direction: rtl;
- .word {
- //flex-direction: row-reverse;
- direction: rtl;
- }
}
&.withLigatures {
.word {
diff --git a/frontend/src/ts/controllers/input-controller.ts b/frontend/src/ts/controllers/input-controller.ts
index c29dd3c28bf2..670c571f0c93 100644
--- a/frontend/src/ts/controllers/input-controller.ts
+++ b/frontend/src/ts/controllers/input-controller.ts
@@ -326,7 +326,6 @@ async function handleSpace(): Promise {
void TestLogic.addWord();
}
TestUI.updateActiveElement();
- void Caret.updatePosition();
const shouldLimitToThreeLines =
Config.mode === "time" ||
@@ -344,8 +343,10 @@ async function handleSpace(): Promise {
if ((nextTop ?? 0) > currentTop) {
void TestUI.lineJump(currentTop);
- } //end of line wrap
- }
+ }
+ } //end of line wrap
+
+ void Caret.updatePosition();
// enable if i decide that auto tab should also work after a space
// if (
diff --git a/frontend/src/ts/elements/result-word-highlight.ts b/frontend/src/ts/elements/result-word-highlight.ts
index 4d771db8c867..bfd4505b2bee 100644
--- a/frontend/src/ts/elements/result-word-highlight.ts
+++ b/frontend/src/ts/elements/result-word-highlight.ts
@@ -4,8 +4,7 @@
// Constants for padding around the highlights
import * as Misc from "../utils/misc";
-import * as JSONData from "../utils/json-data";
-import Config from "../config";
+import * as TestState from "../test/test-state";
const PADDING_X = 16;
const PADDING_Y = 12;
@@ -56,7 +55,6 @@ let isInitialized = false;
let isHoveringChart = false;
let isFirstHighlightSinceInit = true;
let isFirstHighlightSinceClear = true;
-let isLanguageRightToLeft = false;
let isInitInProgress = false;
// Highlights .word elements in range [firstWordIndex, lastWordIndex]
@@ -104,7 +102,7 @@ export async function highlightWordsInRange(
const newHighlightElementPositions = getHighlightElementPositions(
firstWordIndex,
lastWordIndex,
- isLanguageRightToLeft
+ TestState.isLanguageRightToLeft
);
// For each line...
@@ -198,10 +196,6 @@ async function init(): Promise {
);
}
- // Set isLanguageRTL
- const currentLanguage = await JSONData.getCurrentLanguage(Config.language);
- isLanguageRightToLeft = currentLanguage.rightToLeft ?? false;
-
RWH_el = $("#resultWordsHistory")[0] as HTMLElement;
RWH_rect = RWH_el.getBoundingClientRect();
wordEls = $(RWH_el).find(".words .word[input]");
@@ -309,7 +303,7 @@ async function init(): Promise {
// For RTL languages, account for difference between highlightContainer left and RWH_el left
let RTL_offset;
- if (isLanguageRightToLeft) {
+ if (TestState.isLanguageRightToLeft) {
RTL_offset = line.rect.left - RWH_rect.left + PADDING_X;
} else {
RTL_offset = 0;
diff --git a/frontend/src/ts/test/caret.ts b/frontend/src/ts/test/caret.ts
index a8d0aab1b807..ce54653cf042 100644
--- a/frontend/src/ts/test/caret.ts
+++ b/frontend/src/ts/test/caret.ts
@@ -1,11 +1,10 @@
-import * as JSONData from "../utils/json-data";
import Config from "../config";
import * as TestInput from "./test-input";
import * as SlowTimer from "../states/slow-timer";
import * as TestState from "../test/test-state";
import * as TestWords from "./test-words";
import { convertRemToPixels } from "../utils/numbers";
-import { splitIntoCharacters, getWordDirection } from "../utils/strings";
+import { splitIntoCharacters, isWordRightToLeft } from "../utils/strings";
import { safeNumber } from "@monkeytype/util/numbers";
import { subscribe } from "../observables/config-event";
@@ -53,7 +52,6 @@ function getSpaceWidth(wordElement?: HTMLElement): number {
function getTargetPositionLeft(
fullWidthCaret: boolean,
- isLanguageRightToLeft: boolean,
activeWordElement: HTMLElement,
currentWordNodeList: NodeListOf,
fullWidthCaretWidth: number,
@@ -65,9 +63,10 @@ function getTargetPositionLeft(
let result = 0;
// use word-specific direction if available and different from language direction
- const isWordRightToLeft = getWordDirection(
+ const isWordRTL = isWordRightToLeft(
currentWord,
- isLanguageRightToLeft
+ TestState.isLanguageRightToLeft,
+ TestState.isDirectionReversed
);
if (Config.tapeMode === "off") {
@@ -77,7 +76,7 @@ function getTargetPositionLeft(
const lastWordLetter = currentWordNodeList[wordLen - 1];
const lastInputLetter = currentWordNodeList[inputLen - 1];
- if (isWordRightToLeft) {
+ if (isWordRTL) {
if (inputLen <= wordLen && currentLetter) {
// at word beginning in zen mode both lengths are 0, but currentLetter is defined "_"
positionOffsetToWord =
@@ -110,13 +109,10 @@ function getTargetPositionLeft(
$(document.querySelector("#wordsWrapper") as HTMLElement).width() ?? 0;
const tapeMargin =
wordsWrapperWidth *
- (isWordRightToLeft
- ? 1 - Config.tapeMargin / 100
- : Config.tapeMargin / 100);
+ (isWordRTL ? 1 - Config.tapeMargin / 100 : Config.tapeMargin / 100);
result =
- tapeMargin -
- (fullWidthCaret && isWordRightToLeft ? fullWidthCaretWidth : 0);
+ tapeMargin - (fullWidthCaret && isWordRTL ? fullWidthCaretWidth : 0);
if (Config.tapeMode === "word" && inputLen > 0) {
let currentWordWidth = 0;
@@ -131,7 +127,7 @@ function getTargetPositionLeft(
// if current letter has zero width move the caret to previous positive width letter
if ($(currentWordNodeList[inputLen] as Element).outerWidth(true) === 0)
currentWordWidth -= lastPositiveLetterWidth;
- if (isWordRightToLeft) currentWordWidth *= -1;
+ if (isWordRTL) currentWordWidth *= -1;
result += currentWordWidth;
}
}
@@ -185,9 +181,6 @@ export async function updatePosition(noAnim = false): Promise {
const lastInputLetter = currentWordNodeList[inputLen - 1];
const lastWordLetter = currentWordNodeList[wordLen - 1];
- const currentLanguage = await JSONData.getCurrentLanguage(Config.language);
- const isLanguageRightToLeft = currentLanguage.rightToLeft ?? false;
-
// in blind mode, and hide extra letters, extra letters have zero offsets
// offsetHeight is the same for all visible letters
// so is offsetTop (for same line letters)
@@ -224,7 +217,6 @@ export async function updatePosition(noAnim = false): Promise {
const letterPosLeft = getTargetPositionLeft(
fullWidthCaret,
- isLanguageRightToLeft,
activeWordEl,
currentWordNodeList,
letterWidth,
diff --git a/frontend/src/ts/test/pace-caret.ts b/frontend/src/ts/test/pace-caret.ts
index 77c8762ec8ef..a3f367c66195 100644
--- a/frontend/src/ts/test/pace-caret.ts
+++ b/frontend/src/ts/test/pace-caret.ts
@@ -4,12 +4,11 @@ import Config from "../config";
import * as DB from "../db";
import * as SlowTimer from "../states/slow-timer";
import * as Misc from "../utils/misc";
-import * as JSONData from "../utils/json-data";
import * as TestState from "./test-state";
import * as ConfigEvent from "../observables/config-event";
import { convertRemToPixels } from "../utils/numbers";
import { getActiveFunboxes } from "./funbox/list";
-import { getWordDirection } from "../utils/strings";
+import { isWordRightToLeft } from "../utils/strings";
type Settings = {
wpm: number;
@@ -51,22 +50,18 @@ async function resetCaretPosition(): Promise {
if (firstLetter === undefined || firstLetterHeight === undefined) return;
- const currentLanguage = await JSONData.getCurrentLanguage(Config.language);
- const isLanguageRightToLeft = currentLanguage.rightToLeft;
-
const currentWord = TestWords.words.get(settings?.currentWordIndex ?? 0);
- const isWordRightToLeft = getWordDirection(
+ const isWordRTL = isWordRightToLeft(
currentWord,
- isLanguageRightToLeft ?? false
+ TestState.isLanguageRightToLeft,
+ TestState.isDirectionReversed
);
caret.stop(true, true).animate(
{
top: firstLetter.offsetTop - firstLetterHeight / 4,
- left:
- firstLetter.offsetLeft +
- (isWordRightToLeft ? firstLetter.offsetWidth : 0),
+ left: firstLetter.offsetLeft + (isWordRTL ? firstLetter.offsetWidth : 0),
},
0,
"linear"
@@ -238,17 +233,14 @@ export async function update(expectedStepEnd: number): Promise {
);
}
- const currentLanguage = await JSONData.getCurrentLanguage(
- Config.language
- );
- const isLanguageRightToLeft = currentLanguage.rightToLeft;
-
const currentWord = TestWords.words.get(settings.currentWordIndex);
- const isWordRightToLeft = getWordDirection(
+ const isWordRTL = isWordRightToLeft(
currentWord,
- isLanguageRightToLeft ?? false
+ TestState.isLanguageRightToLeft,
+ TestState.isDirectionReversed
);
+
newTop =
word.offsetTop +
currentLetter.offsetTop -
@@ -258,13 +250,13 @@ export async function update(expectedStepEnd: number): Promise {
word.offsetLeft +
currentLetter.offsetLeft -
caretWidth / 2 +
- (isWordRightToLeft ? currentLetterWidth : 0);
+ (isWordRTL ? currentLetterWidth : 0);
} else {
newLeft =
word.offsetLeft +
currentLetter.offsetLeft -
caretWidth / 2 +
- (isWordRightToLeft ? 0 : currentLetterWidth);
+ (isWordRTL ? 0 : currentLetterWidth);
}
caret.removeClass("hidden");
} catch (e) {
diff --git a/frontend/src/ts/test/test-logic.ts b/frontend/src/ts/test/test-logic.ts
index deddad44aad9..083028946b9c 100644
--- a/frontend/src/ts/test/test-logic.ts
+++ b/frontend/src/ts/test/test-logic.ts
@@ -70,6 +70,7 @@ import {
getActiveFunboxes,
getActiveFunboxesWithFunction,
isFunboxActive,
+ isFunboxActiveWithProperty,
} from "./funbox/list";
import { getFunbox } from "@monkeytype/funbox";
import * as CompositionState from "../states/composition";
@@ -408,7 +409,7 @@ let lastInitError: Error | null = null;
let rememberLazyMode: boolean;
let testReinitCount = 0;
-export async function init(): Promise {
+async function init(): Promise {
console.debug("Initializing test");
testReinitCount++;
if (testReinitCount > 3) {
@@ -571,6 +572,13 @@ export async function init(): Promise {
Funbox.toggleScript(TestWords.words.getCurrent());
TestUI.setRightToLeft(language.rightToLeft ?? false);
TestUI.setLigatures(language.ligatures ?? false);
+
+ const isLanguageRTL = language.rightToLeft ?? false;
+ TestState.setIsLanguageRightToLeft(isLanguageRTL);
+ TestState.setIsDirectionReversed(
+ isFunboxActiveWithProperty("reverseDirection")
+ );
+
TestUI.showWords();
console.debug("Test initialized with words", generatedWords);
console.debug(
diff --git a/frontend/src/ts/test/test-state.ts b/frontend/src/ts/test/test-state.ts
index c695b3e11d6b..97a066578e39 100644
--- a/frontend/src/ts/test/test-state.ts
+++ b/frontend/src/ts/test/test-state.ts
@@ -10,6 +10,8 @@ export let selectedQuoteId = 1;
export let activeWordIndex = 0;
export let testInitSuccess = true;
export let lineScrollDistance = 0;
+export let isLanguageRightToLeft = false;
+export let isDirectionReversed = false;
export function setRepeated(tf: boolean): void {
isRepeated = tf;
@@ -58,3 +60,11 @@ export function setTestInitSuccess(tf: boolean): void {
export function setLineScrollDistance(val: number): void {
lineScrollDistance = val;
}
+
+export function setIsLanguageRightToLeft(rtl: boolean): void {
+ isLanguageRightToLeft = rtl;
+}
+
+export function setIsDirectionReversed(val: boolean): void {
+ isDirectionReversed = val;
+}
diff --git a/frontend/src/ts/test/test-ui.ts b/frontend/src/ts/test/test-ui.ts
index 22ea7d322b15..efa1ffa421ee 100644
--- a/frontend/src/ts/test/test-ui.ts
+++ b/frontend/src/ts/test/test-ui.ts
@@ -229,7 +229,7 @@ export function updateActiveElement(
activeWordTop = newActiveWord.offsetTop;
- void updateWordsInputPosition();
+ updateWordsInputPosition();
if (!initial && Config.tapeMode !== "off") {
void scrollTape();
@@ -272,8 +272,11 @@ async function joinOverlappingHints(
activeWordLetters: NodeListOf,
hintElements: HTMLCollection
): Promise {
- const currentLanguage = await JSONData.getCurrentLanguage(Config.language);
- const isLanguageRTL = currentLanguage.rightToLeft;
+ const isWordRightToLeft = Strings.isWordRightToLeft(
+ TestWords.words.getCurrent(),
+ TestState.isLanguageRightToLeft,
+ TestState.isDirectionReversed
+ );
let previousBlocksAdjacent = false;
let currentHintBlock = 0;
@@ -305,8 +308,8 @@ async function joinOverlappingHints(
const sameTop = block1Letter1.offsetTop === block2Letter1.offsetTop;
- const leftBlock = isLanguageRTL ? hintBlock2 : hintBlock1;
- const rightBlock = isLanguageRTL ? hintBlock1 : hintBlock2;
+ const leftBlock = isWordRightToLeft ? hintBlock2 : hintBlock1;
+ const rightBlock = isWordRightToLeft ? hintBlock1 : hintBlock2;
// block edge is offset half its width because of transform: translate(-50%)
const leftBlockEnds = leftBlock.offsetLeft + leftBlock.offsetWidth / 2;
@@ -321,7 +324,7 @@ async function joinOverlappingHints(
const block1Letter1Pos =
block1Letter1.offsetLeft +
- (isLanguageRTL ? block1Letter1.offsetWidth : 0);
+ (isWordRightToLeft ? block1Letter1.offsetWidth : 0);
const bothBlocksLettersWidthHalved =
hintBlock2.offsetLeft - hintBlock1.offsetLeft;
hintBlock1.style.left =
@@ -510,15 +513,16 @@ export function appendEmptyWordElement(
);
}
let updateWordsInputPositionAnimationFrameId: null | number = null;
-export async function updateWordsInputPosition(): Promise {
+export function updateWordsInputPosition(): void {
if (updateWordsInputPositionAnimationFrameId !== null) {
cancelAnimationFrame(updateWordsInputPositionAnimationFrameId);
}
- updateWordsInputPositionAnimationFrameId = requestAnimationFrame(async () => {
+ updateWordsInputPositionAnimationFrameId = requestAnimationFrame(() => {
updateWordsInputPositionAnimationFrameId = null;
if (ActivePage.get() !== "test") return;
- const currentLanguage = await JSONData.getCurrentLanguage(Config.language);
- const isLanguageRTL = currentLanguage.rightToLeft;
+ const isTestRightToLeft = TestState.isDirectionReversed
+ ? !TestState.isLanguageRightToLeft
+ : TestState.isLanguageRightToLeft;
const el = document.querySelector("#wordsInput");
@@ -549,7 +553,7 @@ export async function updateWordsInputPosition(): Promise {
el.style.top = targetTop + "px";
- if (activeWord.offsetWidth < letterHeight && isLanguageRTL) {
+ if (activeWord.offsetWidth < letterHeight && isTestRightToLeft) {
el.style.left = activeWord.offsetLeft - letterHeight + "px";
} else {
el.style.left = Math.max(0, activeWord.offsetLeft) + "px";
@@ -913,8 +917,9 @@ export async function scrollTape(
await centeringActiveLine;
- const currentLang = await JSONData.getCurrentLanguage(Config.language);
- const isLanguageRTL = currentLang.rightToLeft;
+ const isTestRightToLeft = TestState.isDirectionReversed
+ ? !TestState.isLanguageRightToLeft
+ : TestState.isLanguageRightToLeft;
const wordsWrapperWidth = (
document.querySelector("#wordsWrapper") as HTMLElement
@@ -988,8 +993,8 @@ export async function scrollTape(
const forWordLeft = Math.floor(child.offsetLeft);
const forWordWidth = Math.floor(child.offsetWidth);
if (
- (!isLanguageRTL && forWordLeft < 0 - forWordWidth) ||
- (isLanguageRTL && forWordLeft > wordsWrapperWidth)
+ (!isTestRightToLeft && forWordLeft < 0 - forWordWidth) ||
+ (isTestRightToLeft && forWordLeft > wordsWrapperWidth)
) {
toRemove.push(child);
widthRemoved += wordOuterWidth;
@@ -1035,7 +1040,7 @@ export async function scrollTape(
currentLineIndent - (widthRemovedFromLine[i] ?? 0)
}px`;
}
- if (isLanguageRTL) widthRemoved *= -1;
+ if (isTestRightToLeft) widthRemoved *= -1;
const currentWordsMargin = parseFloat(wordsEl.style.marginLeft) || 0;
wordsEl.style.marginLeft = `${currentWordsMargin + widthRemoved}px`;
}
@@ -1068,7 +1073,7 @@ export async function scrollTape(
wordsWrapperWidth * (Config.tapeMargin / 100) -
wordsWidthBeforeActive -
currentWordWidth;
- if (isLanguageRTL) newMargin = wordRightMargin - newMargin;
+ if (isTestRightToLeft) newMargin = wordRightMargin - newMargin;
const jqWords = $(wordsEl);
if (Config.smoothLineScroll) {
diff --git a/frontend/src/ts/ui.ts b/frontend/src/ts/ui.ts
index 3afadcec582f..362d5e036b64 100644
--- a/frontend/src/ts/ui.ts
+++ b/frontend/src/ts/ui.ts
@@ -106,7 +106,7 @@ const debouncedEvent = debounce(250, () => {
void TestUI.updateHintsPositionDebounced();
}
setTimeout(() => {
- void TestUI.updateWordsInputPosition();
+ TestUI.updateWordsInputPosition();
TestUI.focusWords();
}, 250);
}
diff --git a/frontend/src/ts/utils/strings.ts b/frontend/src/ts/utils/strings.ts
index 264d54560633..0786bd3e41d0 100644
--- a/frontend/src/ts/utils/strings.ts
+++ b/frontend/src/ts/utils/strings.ts
@@ -236,26 +236,29 @@ export function clearWordDirectionCache(): void {
wordDirectionCache.clear();
}
-export function getWordDirection(
+export function isWordRightToLeft(
word: string | undefined,
- languageRTL: boolean
+ languageRTL: boolean,
+ reverseDirection?: boolean
): boolean {
- if (word === undefined || word.length === 0) return languageRTL;
+ if (word === undefined || word.length === 0) {
+ return reverseDirection ? !languageRTL : languageRTL;
+ }
// Strip leading/trailing punctuation and whitespace so attached opposite-direction
// punctuation like "word؟" or "،word" doesn't flip the direction detection
// and if only punctuation/symbols/whitespace, use main language direction
const core = word.replace(/^[\p{P}\p{S}\s]+|[\p{P}\p{S}\s]+$/gu, "");
- if (core.length === 0) return languageRTL;
+ if (core.length === 0) return reverseDirection ? !languageRTL : languageRTL;
// cache by core to handle variants like "word" vs "word؟"
const cached = wordDirectionCache.get(core);
- if (cached !== undefined) return cached;
+ if (cached !== undefined) return reverseDirection ? !cached : cached;
const result = hasRTLCharacters(core);
wordDirectionCache.set(core, result);
- return result;
+ return reverseDirection ? !result : result;
}
// Export testing utilities for unit tests
diff --git a/frontend/static/funbox/backwards.css b/frontend/static/funbox/backwards.css
index 2485d2af38a5..10d73f96a7e9 100644
--- a/frontend/static/funbox/backwards.css
+++ b/frontend/static/funbox/backwards.css
@@ -3,5 +3,9 @@
}
#words.rightToLeftTest {
- direction: rtl;
+ direction: ltr;
+}
+
+#words.withLigatures .word {
+ unicode-bidi: bidi-override;
}
diff --git a/packages/contracts/src/results.ts b/packages/contracts/src/results.ts
index e047c0aadd05..642f05b1784a 100644
--- a/packages/contracts/src/results.ts
+++ b/packages/contracts/src/results.ts
@@ -3,6 +3,7 @@ import { z } from "zod";
import {
CommonResponses,
meta,
+ MonkeyClientError,
MonkeyResponseSchema,
responseWithData,
} from "./util/api";
@@ -131,6 +132,13 @@ export const resultsContract = c.router(
body: AddResultRequestSchema.strict(),
responses: {
200: AddResultResponseSchema,
+ 460: MonkeyClientError.describe("Test too short"),
+ 461: MonkeyClientError.describe("Result hash invalid"),
+ 462: MonkeyClientError.describe("Result spacing invalid"),
+ 463: MonkeyClientError.describe("Result data invalid"),
+ 464: MonkeyClientError.describe("Missing key data"),
+ 465: MonkeyClientError.describe("Bot detected"),
+ 466: MonkeyClientError.describe("Duplicate result"),
},
metadata: meta({
rateLimit: "resultsAdd",
diff --git a/packages/contracts/src/util/api.ts b/packages/contracts/src/util/api.ts
index 7697b3e54a66..e9c1de371365 100644
--- a/packages/contracts/src/util/api.ts
+++ b/packages/contracts/src/util/api.ts
@@ -120,6 +120,10 @@ export const CommonResponses = {
403: MonkeyClientError.describe("Operation not permitted"),
422: MonkeyValidationErrorSchema.describe("Request validation failed"),
429: MonkeyClientError.describe("Rate limit exceeded"),
+ 470: MonkeyClientError.describe("Invalid ApeKey"),
+ 471: MonkeyClientError.describe("ApeKey is inactive"),
+ 472: MonkeyClientError.describe("ApeKey is malformed"),
+ 479: MonkeyClientError.describe("ApeKey rate limit exceeded"),
500: MonkeyServerError.describe("Generic server error"),
503: MonkeyServerError.describe(
"Endpoint disabled or server is under maintenance"
diff --git a/packages/funbox/src/list.ts b/packages/funbox/src/list.ts
index eacce0e4b478..2f49d3728cf4 100644
--- a/packages/funbox/src/list.ts
+++ b/packages/funbox/src/list.ts
@@ -404,9 +404,9 @@ const list: Record = {
name: "backwards",
properties: [
"hasCssFile",
- "noLigatures",
"conflictsWithSymmetricChars",
"wordOrder:reverse",
+ "reverseDirection",
],
canGetPb: true,
frontendFunctions: ["alterText"],
diff --git a/packages/funbox/src/types.ts b/packages/funbox/src/types.ts
index 0e0e99d30e29..104174ff3245 100644
--- a/packages/funbox/src/types.ts
+++ b/packages/funbox/src/types.ts
@@ -21,6 +21,7 @@ export type FunboxProperty =
| "noLigatures"
| `toPush:${number}`
| "wordOrder:reverse"
+ | "reverseDirection"
| "ignoreReducedMotion";
type FunboxCSSModification = "typingTest" | "words" | "body" | "main";