Skip to content

Commit acbd1de

Browse files
byseif21Miodec
andauthored
impr(funbox): proper per-language handling in polyglot mode (@byseif21, @fehmer) (monkeytypegame#6666)
### Description #### fixes some polyglot issues * when mixing RTL (e.g. Arabic) with LTR (e.g. English), ligatures were broken if the main `Config.Language` was a LTR lang. * lazy mode: previously, if the main language didn’t support lazy mode, none of the languages would, even if they did individually. closes monkeytypegame#6665 --------- Co-authored-by: Jack <[email protected]>
1 parent b455d49 commit acbd1de

File tree

5 files changed

+160
-27
lines changed

5 files changed

+160
-27
lines changed
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
import { LayoutName, LayoutNameSchema } from "@monkeytype/schemas/layouts";
22

3-
export const LayoutsList:LayoutName[] = LayoutNameSchema._def.values;
3+
export const LayoutsList: LayoutName[] = LayoutNameSchema._def.values;

frontend/src/ts/test/funbox/funbox-functions.ts

Lines changed: 60 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { Section } from "../../utils/json-data";
21
import { FunboxWordsFrequency, Wordset } from "../wordset";
32
import * as GetText from "../../utils/generate";
43
import Config, * as UpdateConfig from "../../config";
@@ -24,17 +23,18 @@ import * as TestState from "../test-state";
2423
import { WordGenError } from "../../utils/word-gen-error";
2524
import { FunboxName, KeymapLayout, Layout } from "@monkeytype/schemas/configs";
2625
import { Language, LanguageObject } from "@monkeytype/schemas/languages";
26+
2727
export type FunboxFunctions = {
2828
getWord?: (wordset?: Wordset, wordIndex?: number) => string;
2929
punctuateWord?: (word: string) => string;
30-
withWords?: (words?: string[]) => Promise<Wordset>;
30+
withWords?: (words?: string[]) => Promise<Wordset | PolyglotWordset>;
3131
alterText?: (word: string, wordIndex: number, wordsBound: number) => string;
3232
applyConfig?: () => void;
3333
applyGlobalCSS?: () => void;
3434
clearGlobal?: () => void;
3535
rememberSettings?: () => void;
3636
toggleScript?: (params: string[]) => void;
37-
pullSection?: (language?: Language) => Promise<Section | false>;
37+
pullSection?: (language?: Language) => Promise<JSONData.Section | false>;
3838
handleSpace?: () => void;
3939
handleChar?: (char: string) => string;
4040
isCharCorrect?: (char: string, originalChar: string) => boolean;
@@ -151,6 +151,23 @@ class PseudolangWordGenerator extends Wordset {
151151
}
152152
}
153153

154+
export class PolyglotWordset extends Wordset {
155+
public wordsWithLanguage: Map<string, Language>;
156+
public languageProperties: Map<Language, JSONData.LanguageProperties>;
157+
158+
constructor(
159+
wordsWithLanguage: Map<string, Language>,
160+
languageProperties: Map<Language, JSONData.LanguageProperties>
161+
) {
162+
// build and shuffle the word array
163+
const wordArray = Array.from(wordsWithLanguage.keys());
164+
Arrays.shuffle(wordArray);
165+
super(wordArray);
166+
this.wordsWithLanguage = wordsWithLanguage;
167+
this.languageProperties = languageProperties;
168+
}
169+
}
170+
154171
const list: Partial<Record<FunboxName, FunboxFunctions>> = {
155172
"58008": {
156173
getWord(): string {
@@ -646,12 +663,12 @@ const list: Partial<Record<FunboxName, FunboxFunctions>> = {
646663
`Failed to load language: ${language}. It will be ignored.`,
647664
0
648665
);
649-
return null; // Return null for failed languages
666+
return null;
650667
})
651668
);
652669

653670
const languages = (await Promise.all(promises)).filter(
654-
(lang) => lang !== null
671+
(lang): lang is LanguageObject => lang !== null
655672
);
656673

657674
if (languages.length === 0) {
@@ -679,9 +696,44 @@ const list: Partial<Record<FunboxName, FunboxFunctions>> = {
679696
throw new WordGenError("");
680697
}
681698

682-
const wordSet = languages.flatMap((it) => it.words);
683-
Arrays.shuffle(wordSet);
684-
return new Wordset(wordSet);
699+
// direction conflict check
700+
const allRightToLeft = languages.every((lang) => lang.rightToLeft);
701+
const allLeftToRight = languages.every((lang) => !lang.rightToLeft);
702+
const mainLanguage = await JSONData.getLanguage(Config.language);
703+
const mainLanguageIsRTL = mainLanguage?.rightToLeft ?? false;
704+
if (
705+
(mainLanguageIsRTL && allLeftToRight) ||
706+
(!mainLanguageIsRTL && allRightToLeft)
707+
) {
708+
const fallbackLanguage =
709+
languages[0]?.name ?? (allRightToLeft ? "arabic" : "english");
710+
UpdateConfig.setLanguage(fallbackLanguage);
711+
Notifications.add(
712+
`Language direction conflict: switched to ${fallbackLanguage} for consistency.`,
713+
0,
714+
{ duration: 5 }
715+
);
716+
throw new WordGenError("");
717+
}
718+
719+
// build languageProperties
720+
const languageProperties = new Map(
721+
languages.map((lang) => [
722+
lang.name,
723+
{
724+
noLazyMode: lang.noLazyMode,
725+
ligatures: lang.ligatures,
726+
rightToLeft: lang.rightToLeft,
727+
additionalAccents: lang.additionalAccents,
728+
},
729+
])
730+
);
731+
732+
const wordsWithLanguage = new Map(
733+
languages.flatMap((lang) => lang.words.map((word) => [word, lang.name]))
734+
);
735+
736+
return new PolyglotWordset(wordsWithLanguage, languageProperties);
685737
},
686738
},
687739
};

frontend/src/ts/test/test-logic.ts

Lines changed: 56 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ import {
6969
findSingleActiveFunboxWithFunction,
7070
getActiveFunboxes,
7171
getActiveFunboxesWithFunction,
72+
getActiveFunboxNames,
7273
isFunboxActive,
7374
isFunboxActiveWithProperty,
7475
} from "./funbox/list";
@@ -469,14 +470,54 @@ async function init(): Promise<boolean> {
469470
}
470471

471472
const allowLazyMode = !language.noLazyMode || Config.mode === "custom";
472-
if (Config.lazyMode && !allowLazyMode) {
473-
rememberLazyMode = true;
474-
Notifications.add("This language does not support lazy mode.", 0, {
475-
important: true,
473+
474+
// polyglot mode, check to enable lazy mode if any support it
475+
if (getActiveFunboxNames().includes("polyglot")) {
476+
const polyglotLanguages = Config.customPolyglot;
477+
const languagePromises = polyglotLanguages.map(async (langName) => {
478+
const { data: lang, error } = await tryCatch(
479+
JSONData.getLanguage(langName)
480+
);
481+
if (error) {
482+
Notifications.add(
483+
Misc.createErrorMessage(
484+
error,
485+
`Failed to load language: ${langName}`
486+
),
487+
-1
488+
);
489+
}
490+
return lang;
476491
});
477-
UpdateConfig.setLazyMode(false, true);
478-
} else if (rememberLazyMode && !language.noLazyMode) {
479-
UpdateConfig.setLazyMode(true, true);
492+
493+
const anySupportsLazyMode = (await Promise.all(languagePromises))
494+
.filter((lang) => lang !== null)
495+
.some((lang) => !lang.noLazyMode);
496+
497+
if (Config.lazyMode && !anySupportsLazyMode) {
498+
rememberLazyMode = true;
499+
Notifications.add(
500+
"None of the selected polyglot languages support lazy mode.",
501+
0,
502+
{
503+
important: true,
504+
}
505+
);
506+
UpdateConfig.setLazyMode(false, true);
507+
} else if (rememberLazyMode && anySupportsLazyMode) {
508+
UpdateConfig.setLazyMode(true, true);
509+
}
510+
} else {
511+
// normal mode
512+
if (Config.lazyMode && !allowLazyMode) {
513+
rememberLazyMode = true;
514+
Notifications.add("This language does not support lazy mode.", 0, {
515+
important: true,
516+
});
517+
UpdateConfig.setLazyMode(false, true);
518+
} else if (rememberLazyMode && !language.noLazyMode) {
519+
UpdateConfig.setLazyMode(true, true);
520+
}
480521
}
481522

482523
if (!Config.lazyMode && !language.noLazyMode) {
@@ -502,16 +543,19 @@ async function init(): Promise<boolean> {
502543
currentQuote: TestWords.currentQuote,
503544
});
504545

505-
let generatedWords: string[];
506-
let generatedSectionIndexes: number[];
507546
let wordsHaveTab = false;
508547
let wordsHaveNewline = false;
548+
let allRightToLeft: boolean | undefined = undefined;
549+
let allLigatures: boolean | undefined = undefined;
550+
let generatedWords: string[] = [];
551+
let generatedSectionIndexes: number[] = [];
509552
try {
510553
const gen = await WordsGenerator.generateWords(language);
511554
generatedWords = gen.words;
512555
generatedSectionIndexes = gen.sectionIndexes;
513556
wordsHaveTab = gen.hasTab;
514557
wordsHaveNewline = gen.hasNewline;
558+
({ allRightToLeft, allLigatures } = gen);
515559
} catch (e) {
516560
Loader.hide();
517561
if (e instanceof WordGenError || e instanceof Error) {
@@ -570,10 +614,10 @@ async function init(): Promise<boolean> {
570614
);
571615
}
572616
Funbox.toggleScript(TestWords.words.getCurrent());
573-
TestUI.setRightToLeft(language.rightToLeft ?? false);
574-
TestUI.setLigatures(language.ligatures ?? false);
617+
TestUI.setLigatures(allLigatures ?? language.ligatures ?? false);
575618

576-
const isLanguageRTL = language.rightToLeft ?? false;
619+
const isLanguageRTL = allRightToLeft ?? language.rightToLeft ?? false;
620+
TestUI.setRightToLeft(isLanguageRTL);
577621
TestState.setIsLanguageRightToLeft(isLanguageRTL);
578622
TestState.setIsDirectionReversed(
579623
isFunboxActiveWithProperty("reverseDirection")

frontend/src/ts/test/words-generator.ts

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import Config, * as UpdateConfig from "../config";
22
import * as CustomText from "./custom-text";
3-
import * as Wordset from "./wordset";
3+
import { Wordset, FunboxWordsFrequency, withWords } from "./wordset";
44
import QuotesController, {
55
Quote,
66
QuoteWithTextSplit,
@@ -24,6 +24,7 @@ import {
2424
} from "./funbox/list";
2525
import { WordGenError } from "../utils/word-gen-error";
2626
import * as Loader from "../elements/loader";
27+
import { PolyglotWordset } from "./funbox/funbox-functions";
2728
import { LanguageObject } from "@monkeytype/schemas/languages";
2829

2930
//pin implementation
@@ -311,7 +312,7 @@ async function applyEnglishPunctuationToWord(word: string): Promise<string> {
311312
return EnglishPunctuation.replace(word);
312313
}
313314

314-
function getFunboxWordsFrequency(): Wordset.FunboxWordsFrequency | undefined {
315+
function getFunboxWordsFrequency(): FunboxWordsFrequency | undefined {
315316
const funbox = findSingleActiveFunboxWithFunction("getWordsFrequencyMode");
316317
if (funbox) {
317318
return funbox.functions.getWordsFrequencyMode();
@@ -345,7 +346,7 @@ async function getFunboxSection(): Promise<string[]> {
345346
function getFunboxWord(
346347
word: string,
347348
wordIndex: number,
348-
wordset?: Wordset.Wordset
349+
wordset?: Wordset
349350
): string {
350351
const funbox = findSingleActiveFunboxWithFunction("getWord");
351352

@@ -384,6 +385,21 @@ async function applyBritishEnglishToWord(
384385
}
385386

386387
function applyLazyModeToWord(word: string, language: LanguageObject): string {
388+
// polyglot mode, use the word's actual language
389+
if (currentWordset && currentWordset instanceof PolyglotWordset) {
390+
const langName = currentWordset.wordsWithLanguage.get(word);
391+
const langProps = langName
392+
? currentWordset.languageProperties.get(langName)
393+
: undefined;
394+
const allowLazyMode =
395+
(langProps && !langProps.noLazyMode) || Config.mode === "custom";
396+
if (Config.lazyMode && allowLazyMode && langProps) {
397+
word = LazyMode.replaceAccents(word, langProps.additionalAccents);
398+
}
399+
return word;
400+
}
401+
402+
// normal mode
387403
const allowLazyMode = !language.noLazyMode || Config.mode === "custom";
388404
if (Config.lazyMode && allowLazyMode) {
389405
word = LazyMode.replaceAccents(word, language.additionalAccents);
@@ -574,7 +590,7 @@ async function getQuoteWordList(
574590
return TestWords.currentQuote.textSplit;
575591
}
576592

577-
let currentWordset: Wordset.Wordset | null = null;
593+
let currentWordset: Wordset | null = null;
578594
let currentLanguage: LanguageObject | null = null;
579595
let isCurrentlyUsingFunboxSection = false;
580596

@@ -583,6 +599,8 @@ type GenerateWordsReturn = {
583599
sectionIndexes: number[];
584600
hasTab: boolean;
585601
hasNewline: boolean;
602+
allRightToLeft?: boolean;
603+
allLigatures?: boolean;
586604
};
587605

588606
let previousRandomQuote: QuoteWithTextSplit | null = null;
@@ -604,6 +622,8 @@ export async function generateWords(
604622
sectionIndexes: [],
605623
hasTab: false,
606624
hasNewline: false,
625+
allRightToLeft: language.rightToLeft,
626+
allLigatures: language.ligatures ?? false,
607627
};
608628

609629
isCurrentlyUsingFunboxSection = isFunboxActiveWithFunction("pullSection");
@@ -634,9 +654,20 @@ export async function generateWords(
634654

635655
const funbox = findSingleActiveFunboxWithFunction("withWords");
636656
if (funbox) {
637-
currentWordset = await funbox.functions.withWords(wordList);
657+
const result = await funbox.functions.withWords(wordList);
658+
// PolyglotWordset if polyglot otherwise Wordset
659+
if (result instanceof PolyglotWordset) {
660+
const polyglotResult = result;
661+
currentWordset = polyglotResult;
662+
// set allLigatures if any language in languageProperties has ligatures true
663+
ret.allLigatures = Array.from(
664+
polyglotResult.languageProperties.values()
665+
).some((props) => !!props.ligatures);
666+
} else {
667+
currentWordset = result;
668+
}
638669
} else {
639-
currentWordset = await Wordset.withWords(wordList);
670+
currentWordset = await withWords(wordList);
640671
}
641672

642673
console.debug("Wordset", currentWordset);

frontend/src/ts/utils/json-data.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,12 @@ export async function getLayout(layoutName: string): Promise<LayoutObject> {
8080
return await cachedFetchJson<LayoutObject>(`/layouts/${layoutName}.json`);
8181
}
8282

83+
// used for polyglot wordset language-specific properties
84+
export type LanguageProperties = Pick<
85+
LanguageObject,
86+
"noLazyMode" | "ligatures" | "rightToLeft" | "additionalAccents"
87+
>;
88+
8389
let currentLanguage: LanguageObject;
8490

8591
/**

0 commit comments

Comments
 (0)