diff --git a/SETUP_GUIDE.md b/SETUP_GUIDE.md new file mode 100644 index 000000000000..34df67f63da4 --- /dev/null +++ b/SETUP_GUIDE.md @@ -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 diff --git a/backend/.npmrc b/backend/.npmrc new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/frontend/.npmrc b/frontend/.npmrc new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/frontend/src/styles/test.scss b/frontend/src/styles/test.scss index d0064f2c7c24..35d596007a86 100644 --- a/frontend/src/styles/test.scss +++ b/frontend/src/styles/test.scss @@ -1,3 +1,5 @@ +@import url("https://fonts.googleapis.com/css2?family=Faruma&display=swap"); + .highlightContainer { position: absolute; overflow: hidden; @@ -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; diff --git a/frontend/src/ts/constants/languages.ts b/frontend/src/ts/constants/languages.ts index fd963dfed479..254977e15bb7 100644 --- a/frontend/src/ts/constants/languages.ts +++ b/frontend/src/ts/constants/languages.ts @@ -368,6 +368,7 @@ export const LanguageGroups: Record = { "code_cuda", ], viossa: ["viossa", "viossa_njutro"], + dhivehi: ["dhivehi"], }; export type LanguageGroupName = keyof typeof LanguageGroups; diff --git a/frontend/src/ts/input/helpers/validation.ts b/frontend/src/ts/input/helpers/validation.ts index 408b5416eba7..0a7514f73be5 100644 --- a/frontend/src/ts/input/helpers/validation.ts +++ b/frontend/src/ts/input/helpers/validation.ts @@ -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 @@ -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); } /** diff --git a/frontend/src/ts/test/test-ui.ts b/frontend/src/ts/test/test-ui.ts index efbe30db3784..23ed25add97d 100644 --- a/frontend/src/ts/test/test-ui.ts +++ b/frontend/src/ts/test/test-ui.ts @@ -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+/) @@ -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 = ""; diff --git a/frontend/src/ts/test/words-generator.ts b/frontend/src/ts/test/words-generator.ts index c578bc4dfd08..e51991083a7a 100644 --- a/frontend/src/ts/test/words-generator.ts +++ b/frontend/src/ts/test/words-generator.ts @@ -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") { @@ -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 += ";"; @@ -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") { diff --git a/frontend/src/ts/utils/strings.ts b/frontend/src/ts/utils/strings.ts index 4ec735b670ef..6334537e2a6a 100644 --- a/frontend/src/ts/utils/strings.ts +++ b/frontend/src/ts/utils/strings.ts @@ -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 = { + "،": ",", + "؛": ";", + "؟": "?", +}; + +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; } /** @@ -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]+/; diff --git a/frontend/static/languages/dhivehi.json b/frontend/static/languages/dhivehi.json new file mode 100644 index 000000000000..c51d1a508d61 --- /dev/null +++ b/frontend/static/languages/dhivehi.json @@ -0,0 +1,108 @@ +{ + "name": "dhivehi", + "rightToLeft": true, + "ligatures": false, + "orderedByFrequency": true, + "bcp47": "dv", + "words": [ + "އެ", + "މި", + "ގެ", + "ވާ", + "ކަން", + "ދެން", + "ހެން", + "ކުރި", + "ހުރި", + "ތިބި", + "ނެތް", + "ވެސް", + "އިން", + "އަށް", + "ނޫން", + "އެއް", + "މީހުން", + "ބޭނުން", + "ދިވެހި", + "މާލެ", + "ރާއްޖެ", + "ގޮތް", + "ކަމެއް", + "އެހެން", + "ކޮށް", + "ފައިސާ", + "މަސް", + "އަހަރު", + "ދުވަސް", + "ވަގުތު", + "ގަޑި", + "ފެން", + "ކާނާ", + "ބަތް", + "ރިހަ", + "ރޮށި", + "ސައި", + "ކިރު", + "ރަށް", + "މޫދު", + "ކަނޑު", + "އިރު", + "ހަނދު", + "ތަރި", + "އުޑު", + "ބިން", + "ރުއް", + "ގަސް", + "މާ", + "ފަތް", + "މޭވާ", + "ކާށި", + "އަނބު", + "ކެޔޮ", + "ލޮނު", + "ހަކުރު", + "ތެޔޮ", + "ރަހަ", + "ފޮނި", + "ހުތް", + "ކުޅި", + "ހިތި", + "ކުލަ", + "ރަތް", + "ނޫ", + "ފެހި", + "ކަޅު", + "ހުދު", + "މުށި", + "ދޭއް", + "ބޮޑު", + "ކުޑަ", + "ދިގު", + "ކުރު", + "ފުޅާ", + "ހަނި", + "ބަރު", + "ލުއި", + "ފޯނު", + "ޓީވީ", + "ލަވަ", + "ވޮލީ", + "ވައި", + "ވާރޭ", + "އަވި", + "ހޫނު", + "ފިނި", + "ކެރި", + "ގަދަ", + "މަޑު", + "ނިދި", + "ބަލި", + "ބޭސް", + "މަގު", + "ބުރު", + "ކާރު", + "ބަސް", + "ދީން", + "ހާސް" + ] +} diff --git a/frontend/static/layouts/dhivehi.json b/frontend/static/layouts/dhivehi.json new file mode 100644 index 000000000000..a3322847c0ae --- /dev/null +++ b/frontend/static/layouts/dhivehi.json @@ -0,0 +1,62 @@ +{ + "keymapShowTopRow": false, + "type": "ansi", + "keys": { + "row1": [ + ["`", "~"], + ["1", "!"], + ["2", "@"], + ["3", "#"], + ["4", "$"], + ["5", "%"], + ["6", "^"], + ["7", "&"], + ["8", "*"], + ["9", ")"], + ["0", "("], + ["-", "_"], + ["=", "+"] + ], + "row2": [ + ["ޮ", "ޯ"], + ["އ", "ޢ"], + ["ެ", "ޭ"], + ["ރ", "ޜ"], + ["ތ", "ޓ"], + ["ޔ", "ޠ"], + ["ު", "ޫ"], + ["ި", "ީ"], + ["ޮ", "ޯ"], + ["ޕ", "÷"], + ["[", "{"], + ["]", "}"], + ["\\", "|"] + ], + "row3": [ + ["ަ", "ާ"], + ["ސ", "ށ"], + ["ދ", "ޑ"], + ["ފ", "ﷲ"], + ["ގ", "ޣ"], + ["ހ", "ޙ"], + ["ޖ", "ޛ"], + ["ކ", "ޚ"], + ["ލ", "ޅ"], + ["؛", ":"], + ["'", "\""] + ], + "row4": [ + ["ޒ", "ޡ"], + ["×", "ޘ"], + ["ޗ", "ޝ"], + ["ވ", "ޥ"], + ["ބ", "ޞ"], + ["ނ", "ޏ"], + ["މ", "ޟ"], + ["،", "ޤ"], + [".", ">"], + ["/", "؟"] + ], + "row5": [[" "]] + } +} diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index 68a5f3108371..53a19e315ebd 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -52,6 +52,8 @@ export default defineConfig(({ mode }): UserConfig => { open: env["SERVER_OPEN"] !== "false", port: 3000, host: env["BACKEND_URL"] !== undefined, + // Disable host header validation only when explicitly requested for special dev setups + allowedHosts: env["DISABLE_HOST_VALIDATION"] === "true" ? true : undefined, watch: { //we rebuild the whole contracts package when a file changes //so we only want to watch one file diff --git a/packages/schemas/src/languages.ts b/packages/schemas/src/languages.ts index 7e26911a3c91..d076f17dcf8d 100644 --- a/packages/schemas/src/languages.ts +++ b/packages/schemas/src/languages.ts @@ -430,6 +430,7 @@ export const LanguageSchema = z.enum( "code_abap_1k", "code_yoptascript", "code_cuda", + "dhivehi", ], { errorMap: customEnumErrorHandler("Must be a supported language"), diff --git a/packages/schemas/src/layouts.ts b/packages/schemas/src/layouts.ts index 94de81ef32df..8793f1b91fb3 100644 --- a/packages/schemas/src/layouts.ts +++ b/packages/schemas/src/layouts.ts @@ -96,6 +96,7 @@ export const LayoutNameSchema = z.enum( "arabic_101", "arabic_102", "arabic_mac", + "dhivehi", "hebrew", "urdu_phonetic", "brasileiro_nativo",