Skip to content

Commit eb092ce

Browse files
authored
refactor: move languages to contracts (@fehmer) (monkeytypegame#6497)
1 parent ed24f7f commit eb092ce

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

50 files changed

+1296
-1698
lines changed

backend/__tests__/api/controllers/leaderboard.spec.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,7 @@ describe("Loaderboard Controller", () => {
188188
expect(body).toEqual({
189189
message: "Invalid query schema",
190190
validationErrors: [
191-
'"language" Can only contain letters [a-zA-Z0-9_+]',
191+
'"language" Invalid enum value. Must be a supported language',
192192
`"mode" Invalid enum value. Expected 'time' | 'words' | 'quote' | 'custom' | 'zen', received 'unknownMode'`,
193193
'"mode2" Needs to be a number or a number represented as a string e.g. "10".',
194194
'"page" Number must be greater than or equal to 0',
@@ -344,7 +344,7 @@ describe("Loaderboard Controller", () => {
344344
expect(body).toEqual({
345345
message: "Invalid query schema",
346346
validationErrors: [
347-
'"language" Can only contain letters [a-zA-Z0-9_+]',
347+
'"language" Invalid enum value. Must be a supported language',
348348
`"mode" Invalid enum value. Expected 'time' | 'words' | 'quote' | 'custom' | 'zen', received 'unknownMode'`,
349349
'"mode2" Needs to be a number or a number represented as a string e.g. "10".',
350350
],
@@ -652,7 +652,7 @@ describe("Loaderboard Controller", () => {
652652
expect(body).toEqual({
653653
message: "Invalid query schema",
654654
validationErrors: [
655-
'"language" Can only contain letters [a-zA-Z0-9_+]',
655+
'"language" Invalid enum value. Must be a supported language',
656656
`"mode" Invalid enum value. Expected 'time' | 'words' | 'quote' | 'custom' | 'zen', received 'unknownMode'`,
657657
'"mode2" Needs to be a number or a number represented as a string e.g. "10".',
658658
],
@@ -830,7 +830,7 @@ describe("Loaderboard Controller", () => {
830830
expect(body).toEqual({
831831
message: "Invalid query schema",
832832
validationErrors: [
833-
'"language" Can only contain letters [a-zA-Z0-9_+]',
833+
'"language" Invalid enum value. Must be a supported language',
834834
`"mode" Invalid enum value. Expected 'time' | 'words' | 'quote' | 'custom' | 'zen', received 'unknownMode'`,
835835
'"mode2" Needs to be a number or a number represented as a string e.g. "10".',
836836
],

backend/__tests__/api/controllers/public.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ describe("PublicController", () => {
8888
expect(body).toEqual({
8989
message: "Invalid query schema",
9090
validationErrors: [
91-
'"language" Can only contain letters [a-zA-Z0-9_+]',
91+
'"language" Invalid enum value. Must be a supported language',
9292
`"mode" Invalid enum value. Expected 'time' | 'words' | 'quote' | 'custom' | 'zen', received 'unknownMode'`,
9393
'"mode2" Needs to be a number or a number represented as a string e.g. "10".',
9494
],

backend/src/api/controllers/dev.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import { roundTo2 } from "@monkeytype/util/numbers";
2222
import { MonkeyRequest } from "../types";
2323
import { DBResult } from "../../utils/result";
2424
import { LbPersonalBests } from "../../utils/pb";
25+
import { Language } from "@monkeytype/contracts/schemas/languages";
2526

2627
const CREATE_RESULT_DEFAULT_OPTIONS = {
2728
firstTestTimestamp: DateUtils.startOfDay(new UTCDate(Date.now())).valueOf(),
@@ -207,7 +208,7 @@ async function updateUser(uid: string): Promise<void> {
207208
const modes = stats.map(
208209
(it) =>
209210
it["_id"] as {
210-
language: string;
211+
language: Language;
211212
mode: "time" | "custom" | "words" | "quote" | "zen";
212213
mode2: `${number}` | "custom" | "zen";
213214
}

backend/src/api/controllers/quote.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import {
2323
} from "@monkeytype/contracts/quotes";
2424
import { replaceObjectId, replaceObjectIds } from "../../utils/misc";
2525
import { MonkeyRequest } from "../types";
26+
import { Language } from "@monkeytype/contracts/schemas/languages";
2627

2728
async function verifyCaptcha(captcha: string): Promise<void> {
2829
if (!(await verify(captcha))) {
@@ -36,9 +37,9 @@ export async function getQuotes(
3637
const { uid } = req.ctx.decodedToken;
3738
const quoteMod = (await getPartialUser(uid, "get quotes", ["quoteMod"]))
3839
.quoteMod;
39-
const quoteModString = quoteMod === true ? "all" : (quoteMod as string);
40+
const quoteModLanguage = quoteMod === true ? "all" : (quoteMod as Language);
4041

41-
const data = await NewQuotesDAL.get(quoteModString);
42+
const data = await NewQuotesDAL.get(quoteModLanguage);
4243
return new MonkeyResponse(
4344
"Quote submissions retrieved",
4445
replaceObjectIds(data)

backend/src/dal/new-quotes.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { WithObjectId } from "../utils/misc";
1111
import { parseWithSchema as parseJsonWithSchema } from "@monkeytype/util/json";
1212
import { z } from "zod";
1313
import { tryCatchSync } from "@monkeytype/util/trycatch";
14+
import { Language } from "@monkeytype/contracts/schemas/languages";
1415

1516
const JsonQuoteSchema = z.object({
1617
text: z.string(),
@@ -112,11 +113,11 @@ export async function add(
112113
return undefined;
113114
}
114115

115-
export async function get(language: string): Promise<DBNewQuote[]> {
116+
export async function get(language: Language | "all"): Promise<DBNewQuote[]> {
116117
if (git === undefined) throw new MonkeyError(500, "Git not available.");
117118
const where: {
118119
approved: boolean;
119-
language?: string;
120+
language?: Language;
120121
} = {
121122
approved: false,
122123
};

backend/src/dal/quote-ratings.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { QuoteRating } from "@monkeytype/contracts/schemas/quotes";
22
import * as db from "../init/db";
33
import { Collection } from "mongodb";
44
import { WithObjectId } from "../utils/misc";
5+
import { Language } from "@monkeytype/contracts/schemas/languages";
56

67
type DBQuoteRating = WithObjectId<QuoteRating>;
78

@@ -11,7 +12,7 @@ export const getQuoteRatingCollection = (): Collection<DBQuoteRating> =>
1112

1213
export async function submit(
1314
quoteId: number,
14-
language: string,
15+
language: Language,
1516
rating: number,
1617
update: boolean
1718
): Promise<void> {
@@ -47,7 +48,7 @@ export async function submit(
4748

4849
export async function get(
4950
quoteId: number,
50-
language: string
51+
language: Language
5152
): Promise<DBQuoteRating | null> {
5253
return await getQuoteRatingCollection().findOne({ quoteId, language });
5354
}

backend/src/middlewares/permission.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ const permissionChecks: Record<PermissionId, PermissionCheck> = {
5959
["quoteMod"],
6060
(user) =>
6161
user.quoteMod === true ||
62-
(typeof user.quoteMod === "string" && user.quoteMod !== "")
62+
(typeof user.quoteMod === "string" && (user.quoteMod as string) !== "")
6363
),
6464
canReport: buildUserPermission(
6565
["canReport"],

docs/LANGUAGES.md

Lines changed: 19 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -31,35 +31,28 @@ The contents of the file should be as follows:
3131
It is recommended that you familiarize yourselves with JSON before adding a language. For the `name` field, put the name of your language. `rightToLeft` indicates how the language is written. If it is written right to left then put `true`, otherwise put `false`.
3232
`ligatures` A ligature occurs when multiple letters are joined together to form a character [more details](<https://en.wikipedia.org/wiki/Ligature_(writing)>). If there's joining in the words, which is the case in languages like (Arabic, Malayalam, Persian, Sanskrit, Central_Kurdish... etc.), then set the value to `true`, otherwise set it to `false`. For `bcp47` put your languages [IETF language tag](https://en.wikipedia.org/wiki/IETF_language_tag). If the words you're adding are ordered by frequency (most common words at the top, least at the bottom) set the value of `orderedByFrequency` to `true`, otherwise `false`. Finally, add your list of words to the `words` field.
3333

34-
In addition to the language file, you need to add your language to the `_groups.json` and `_list.json` files in the same directory. Add the name of the language to the `_groups.json` file like so:
35-
36-
```json
37-
{
38-
"name": "spanish",
39-
"languages": ["spanish", "spanish_1k", "spanish_10k"]
40-
},
41-
{
42-
"name": "YOUR_LANGUAGE",
43-
"languages": ["YOUR_LANGUAGES"]
44-
},
45-
{
46-
"name": "french",
47-
"languages": ["french", "french_1k", "french_2k", "french_10k"]
48-
},
34+
Then, go to `packages/contracts/src/schemas/languages.ts` and add your new language name at the _end_ of the `LanguageSchema` enum. Make sure to end the line with a comma. Make sure to add all your language names if you have created multiple word lists of differing lengths in the same language.
35+
36+
```typescript
37+
export const LanguageSchema = z.enum([
38+
"english",
39+
"english_1k",
40+
...
41+
"your_language_name",
42+
"your_language_name_10k",
43+
]);
4944
```
5045

51-
The `languages` field is the list of files that you have created for your language (without the `.json` file extension). Make sure to add all your files if you have created multiple word lists of differing lengths in the same language.
52-
53-
Add your language lists to the `_list.json` file like so:
46+
Then, go to `frontend/src/ts/constants/language.ts` and add your new language name to the `LanguageGroups` map. You can either add it to an existing group or add a new one. Make sure to add all your language names if you have created multiple word lists of differing lengths in the same language.
5447

55-
```json
56-
,"spanish"
57-
,"spanish_1k"
58-
,"spanish_10k"
59-
,"YOUR_LANGUAGE"
60-
,"french"
61-
,"french_1k"
62-
,"french_2k"
48+
```typescript
49+
export const LanguageGroups: Record<string, Language[]> = {
50+
...
51+
your_language_name: [
52+
"your_language_name",
53+
"your_language_name_10k",
54+
]
55+
};
6356
```
6457

6558
### Committing Languages
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { readdirSync } from "fs";
2+
import { LanguageGroups, LanguageList } from "../../src/ts/constants/languages";
3+
import { Language } from "@monkeytype/contracts/schemas/languages";
4+
5+
describe("languages", () => {
6+
describe("LanguageList", () => {
7+
it("should not have duplicates", () => {
8+
const duplicates = LanguageList.filter(
9+
(item, index) => LanguageList.indexOf(item) !== index
10+
);
11+
expect(duplicates).toEqual([]);
12+
});
13+
it("should have all related json files", () => {
14+
const languageFiles = listLanguageFiles();
15+
16+
const missingLanguageFiles = LanguageList.filter(
17+
(it) => !languageFiles.includes(it)
18+
).map((it) => `fontend/static/languages/${it}.json`);
19+
20+
expect(missingLanguageFiles, "missing language json files").toEqual([]);
21+
});
22+
it("should not have additional related json files", () => {
23+
const LanguageFiles = listLanguageFiles();
24+
25+
const additionalLanguageFiles = LanguageFiles.filter(
26+
(it) => !LanguageList.some((language) => language === it)
27+
).map((it) => `fontend/static/languages/${it}.json`);
28+
29+
expect(
30+
additionalLanguageFiles,
31+
"additional language json files not declared in frontend/src/ts/constants/languages.ts"
32+
).toEqual([]);
33+
});
34+
});
35+
describe("LanguageGroups", () => {
36+
it("should contain each language once", () => {
37+
const languagesWithMultipleGroups = [];
38+
39+
const groupByLanguage = new Map<Language, string>();
40+
41+
for (const group of Object.keys(LanguageGroups)) {
42+
for (const language of LanguageGroups[group] as Language[]) {
43+
if (groupByLanguage.has(language)) {
44+
languagesWithMultipleGroups.push(language);
45+
}
46+
groupByLanguage.set(language, group);
47+
}
48+
}
49+
50+
expect(
51+
languagesWithMultipleGroups,
52+
"languages with multiple groups"
53+
).toEqual([]);
54+
55+
expect(
56+
Array.from(groupByLanguage.keys()).sort(),
57+
"every language has a group"
58+
).toEqual(LanguageList.sort());
59+
});
60+
});
61+
});
62+
63+
function listLanguageFiles() {
64+
return readdirSync(import.meta.dirname + "/../../static/languages").map(
65+
(it) => it.substring(0, it.length - 5) as Language
66+
);
67+
}

frontend/__tests__/root/config.spec.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -423,11 +423,8 @@ describe("Config", () => {
423423
it("setLanguage", () => {
424424
expect(Config.setLanguage("english")).toBe(true);
425425
expect(Config.setLanguage("english_1k")).toBe(true);
426-
expect(Config.setLanguage(stringOfLength(50))).toBe(true);
427426

428-
expect(Config.setLanguage("english 1k")).toBe(false);
429-
expect(Config.setLanguage("english-1k")).toBe(false);
430-
expect(Config.setLanguage(stringOfLength(51))).toBe(false);
427+
expect(Config.setLanguage("invalid" as any)).toBe(false);
431428
});
432429
it("setKeymapLayout", () => {
433430
expect(Config.setKeymapLayout("overrideSync")).toBe(true);

0 commit comments

Comments
 (0)