Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
113 changes: 113 additions & 0 deletions SETUP_GUIDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
# Monkeytype Local Setup Guide

## Prerequisites

- **Node.js 24.11.0** (or 22.21.0) - Install from https://nodejs.org/ or use nvm
- **PNPM 9.6.0** - Run: `npm install -g pnpm@9.6.0`
- **Docker Desktop** - Install from https://www.docker.com/get-started/
- **Git** - Install from https://git-scm.com/

## Setup Steps

### Step 1: Clone the Repository

```bash
git clone https://github.com/monkeytypegame/monkeytype.git
cd monkeytype
```

### Step 2: Install Dependencies

```bash
pnpm install
```

_Note: If you get Node version errors, create `.npmrc` files with `engine-strict=false` in root, backend, and frontend directories._

### Step 3: Configure Backend

```bash
# Copy environment file
copy backend\example.env backend\.env

# The .env file already has MODE=dev set, which is what you need
```

### Step 4: Create Firebase Config (Optional)

Create `frontend/src/ts/constants/firebase-config.ts`:

```typescript
export const firebaseConfig = {
apiKey: "",
authDomain: "",
projectId: "",
storageBucket: "",
messagingSenderId: "",
appId: "",
};
```

_Leave empty for development without authentication features._

### Step 5: Start Databases

```bash
cd backend
npm run docker-db-only
```

_This starts MongoDB (port 27017) and Redis (port 6379) in Docker containers._

### Step 6: Start Backend Server

Open a new terminal:

```bash
cd backend
npm run dev
```

_Backend will run on http://localhost:5005_

### Step 7: Start Frontend

Open another new terminal:

```bash
npm run dev-fe
```

_Frontend will run on http://localhost:3000_

### Step 8: Open Application

Visit **http://localhost:3000** in your browser!

## Quick Start Commands

After initial setup, you only need:

1. `cd backend && npm run docker-db-only` (start databases)
2. `cd backend && npm run dev` (start backend)
3. `npm run dev-fe` (start frontend)

## Troubleshooting

**Node version error?**

- Use `nvm use 24.11.0` or create `.npmrc` files with `engine-strict=false`

**Backend won't connect to database?**

- Ensure Docker Desktop is running
- Check databases are running: `docker ps`

**Firebase error on frontend?**

- This is normal if you haven't set up Firebase
- You can still use all typing features without authentication

**Port already in use?**

- Stop other processes using ports 3000, 5005, 27017, or 6379
Empty file added backend/.npmrc
Empty file.
Empty file added frontend/.npmrc
Empty file.
14 changes: 14 additions & 0 deletions frontend/src/styles/test.scss
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
@import url("https://fonts.googleapis.com/css2?family=Faruma&display=swap");

.highlightContainer {
position: absolute;
overflow: hidden;
Expand Down Expand Up @@ -324,6 +326,18 @@
unicode-bidi: bidi-override;
}
}

&.thaanaTest {
font-family: "Faruma", "MV Faseyha", sans-serif;

.word letter {
display: inline-block;
unicode-bidi: isolate;
vertical-align: baseline;
line-height: 1;
}
}

&.withLigatures {
.word {
overflow-wrap: anywhere;
Expand Down
1 change: 1 addition & 0 deletions frontend/src/ts/constants/languages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,7 @@ export const LanguageGroups: Record<string, Language[]> = {
"code_cuda",
],
viossa: ["viossa", "viossa_njutro"],
dhivehi: ["dhivehi"],
};

export type LanguageGroupName = keyof typeof LanguageGroups;
Expand Down
22 changes: 15 additions & 7 deletions frontend/src/ts/input/helpers/validation.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import Config from "../../config";
import { isSpace } from "../../utils/strings";
import {
isSpace,
splitIntoCharacters,
isCharacterMatch,
} from "../../utils/strings";

/**
* Check if the input data is correct
Expand Down Expand Up @@ -29,17 +33,21 @@ export function isCharCorrect(options: {
return inputValue === targetWord;
}

const targetChar = targetWord[inputValue.length];
// Use splitIntoCharacters to properly handle combining characters (like Thaana fili)
const inputChars = splitIntoCharacters(inputValue + data);
const targetChars = splitIntoCharacters(targetWord);

// Get the character we just typed (last in inputChars after combining)
const typedCharIndex = inputChars.length - 1;
const typedChar = inputChars[typedCharIndex];
const targetChar = targetChars[typedCharIndex];

if (targetChar === undefined) {
return false;
}

if (data === targetChar) {
return true;
}

return false;
// Use isCharacterMatch to handle partial matches (e.g., Thaana consonant before vowel mark)
return isCharacterMatch(typedChar ?? "", targetChar);
}

/**
Expand Down
19 changes: 18 additions & 1 deletion frontend/src/ts/test/test-ui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -447,6 +447,17 @@ function updateWordWrapperClasses(): void {
$("#resultReplay .words").removeClass("rightToLeftTest");
}

// Add special handling for Thaana (Dhivehi) script
if (Config.language.startsWith("dhivehi")) {
wordsEl.classList.add("thaanaTest");
$("#resultWordsHistory .words").addClass("thaanaTest");
$("#resultReplay .words").addClass("thaanaTest");
} else {
wordsEl.classList.remove("thaanaTest");
$("#resultWordsHistory .words").removeClass("thaanaTest");
$("#resultReplay .words").removeClass("thaanaTest");
}

const existing =
wordsEl?.className
.split(/\s+/)
Expand Down Expand Up @@ -749,8 +760,14 @@ export async function updateWordLetters({

const inputChars = Strings.splitIntoCharacters(input);
const currentWordChars = Strings.splitIntoCharacters(currentWord);

for (let i = 0; i < inputChars.length; i++) {
const charCorrect = currentWordChars[i] === inputChars[i];
const inputChar = inputChars[i] ?? "";
const targetChar = currentWordChars[i] ?? "";

const charCorrect =
inputChar === targetChar ||
Strings.isCharacterMatchForDisplay(inputChar, targetChar);

let currentLetter = currentWordChars[i] as string;
let tabChar = "";
Expand Down
12 changes: 9 additions & 3 deletions frontend/src/ts/test/words-generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,8 @@ export async function punctuateWord(
currentLanguage === "arabic" ||
currentLanguage === "persian" ||
currentLanguage === "urdu" ||
currentLanguage === "kurdish"
currentLanguage === "kurdish" ||
currentLanguage === "dhivehi"
) {
word += "؟";
} else if (currentLanguage === "greek") {
Expand Down Expand Up @@ -216,7 +217,11 @@ export async function punctuateWord(
// However, a) it has fallen into disuse in contemporary times and
// b) there isn't a dedicated key on a keyboard to input it
word = ".";
} else if (currentLanguage === "arabic" || currentLanguage === "kurdish") {
} else if (
currentLanguage === "arabic" ||
currentLanguage === "kurdish" ||
currentLanguage === "dhivehi"
) {
word += "؛";
} else if (currentLanguage === "chinese") {
word += ";";
Expand All @@ -228,7 +233,8 @@ export async function punctuateWord(
currentLanguage === "arabic" ||
currentLanguage === "urdu" ||
currentLanguage === "persian" ||
currentLanguage === "kurdish"
currentLanguage === "kurdish" ||
currentLanguage === "dhivehi"
) {
word += "،";
} else if (currentLanguage === "japanese") {
Expand Down
82 changes: 73 additions & 9 deletions frontend/src/ts/utils/strings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,18 +176,82 @@ export function cleanTypographySymbols(textToClean: string): string {
}

/**
* Split a string into characters. This supports multi-byte characters outside of the [Basic Multilinugal Plane](https://en.wikipedia.org/wiki/Plane_(Unicode).
* Using `string.length` and `string[index]` does not work.
* @param s string to be tokenized into characters
* @returns array of characters
* Check if a character is a Thaana combining vowel mark (fili) - U+07A6 to U+07B0
*/
export function isThaanaCombiningMark(char: string): boolean {
const code = char.charCodeAt(0);
return code >= 0x07a6 && code <= 0x07b0;
}

/**
* Check if a character is a Thaana consonant - U+0780 to U+07A5
*/
export function isThaanaConsonant(char: string): boolean {
const code = char.charCodeAt(0);
return code >= 0x0780 && code <= 0x07a5;
}

/**
* Split a string into individual Unicode code points.
*/
export function splitIntoCharacters(s: string): string[] {
const result: string[] = [];
for (const t of s) {
result.push(t);
// eslint-disable-next-line @typescript-eslint/no-misused-spread -- Intentional use of spread to split into Unicode code points (used by Thaana and other scripts)
return [...s];
}

const punctuationEquivalents: Record<string, string> = {
"،": ",",
"؛": ";",
"؟": "?",
};

function arePunctuationEquivalent(char1: string, char2: string): boolean {
if (char1 === char2) return true;
if (punctuationEquivalents[char1] === char2) return true;
if (punctuationEquivalents[char2] === char1) return true;
return false;
}

/**
* Check if input matches target, including partial Thaana matches and punctuation equivalents.
*/
export function isCharacterMatch(
inputChar: string,
targetChar: string,
): boolean {
if (inputChar === targetChar) return true;

if (inputChar.length === 1 && targetChar.length === 1) {
if (arePunctuationEquivalent(inputChar, targetChar)) return true;
}

if (
inputChar.length === 1 &&
targetChar.length === 2 &&
isThaanaConsonant(inputChar) &&
targetChar.startsWith(inputChar) &&
isThaanaCombiningMark(targetChar.charAt(1))
) {
return true;
}

return false;
}

/**
* Check if input matches target for display (no partial Thaana matches).
*/
export function isCharacterMatchForDisplay(
inputChar: string,
targetChar: string,
): boolean {
if (inputChar === targetChar) return true;

if (inputChar.length === 1 && targetChar.length === 1) {
if (arePunctuationEquivalent(inputChar, targetChar)) return true;
}

return result;
return false;
}

/**
Expand Down Expand Up @@ -219,7 +283,7 @@ function hasRTLCharacters(word: string): [boolean, number] {
return [false, 0];
}

// This covers Arabic, Farsi, Urdu, and other RTL scripts
// This covers Arabic, Farsi, Urdu, Hebrew, Thaana (Dhivehi), and other RTL scripts
const rtlPattern =
/[\u0590-\u05FF\u0600-\u06FF\u0750-\u077F\u08A0-\u08FF\uFB50-\uFDFF\uFE70-\uFEFF]+/;

Expand Down
Loading
Loading