From bdcf28bbad53d6d0dfc99053c26515aa3c704746 Mon Sep 17 00:00:00 2001 From: Xavier <0xavier0@gmail.com> Date: Sat, 15 Nov 2025 08:11:38 -0500 Subject: [PATCH 1/4] Feat/hebrew qwerty layout (#1) * Add keyboard layout selection and update keyboard configurations for multiple languages * Refactor keyboard layout selector form for improved styling --- webapp/app.py | 138 +++++++++++++++---- webapp/data/languages/en/en_keyboard.json | 158 +++++++++++++++++----- webapp/data/languages/he/he_keyboard.json | 86 +++++++++++- webapp/templates/game.html | 20 ++- 4 files changed, 335 insertions(+), 67 deletions(-) diff --git a/webapp/app.py b/webapp/app.py index 163c6dbf..d7223704 100644 --- a/webapp/app.py +++ b/webapp/app.py @@ -116,12 +116,49 @@ def load_language_config(lang): def load_keyboard(lang): + keyboard_path = f"{data_dir}languages/{lang}/{lang}_keyboard.json" try: - with open(f"{data_dir}languages/{lang}/{lang}_keyboard.json", "r") as f: - keyboard = json.load(f) - return keyboard - except: - return [] + with open(keyboard_path, "r") as f: + keyboard_data = json.load(f) + except FileNotFoundError: + return {"default": None, "layouts": {}} + except Exception: + return {"default": None, "layouts": {}} + + if isinstance(keyboard_data, list): + if not keyboard_data: + return {"default": None, "layouts": {}} + return { + "default": "default", + "layouts": {"default": {"label": "Default", "rows": keyboard_data}}, + } + + if not isinstance(keyboard_data, dict): + return {"default": None, "layouts": {}} + + layouts_block = keyboard_data.get("layouts") + if isinstance(layouts_block, dict): + source_layouts = layouts_block + else: + source_layouts = { + key: value for key, value in keyboard_data.items() if key != "default" + } + + normalized_layouts = {} + for layout_name, layout_value in source_layouts.items(): + if isinstance(layout_value, dict): + rows = layout_value.get("rows", []) + label = layout_value.get("label") or layout_name.replace("_", " ").title() + else: + rows = layout_value + label = layout_name.replace("_", " ").title() + normalized_layouts[layout_name] = {"label": label, "rows": rows} + + default_layout = keyboard_data.get("default") + if default_layout not in normalized_layouts: + default_layout = next(iter(normalized_layouts), None) + + return {"default": default_layout, "layouts": normalized_layouts} def get_todays_idx(): @@ -199,7 +236,7 @@ def load_languages(): class Language: """Holds the attributes of a language""" - def __init__(self, language_code, word_list): + def __init__(self, language_code, word_list, keyboard_layout=None): self.language_code = language_code self.word_list = word_list self.word_list_supplement = language_codes_5words_supplements[language_code] @@ -216,27 +253,59 @@ def __init__(self, language_code, word_list): characters_used = list(set(characters_used)) self.characters = [char for char in self.characters if char in characters_used] - self.keyboard = keyboards[language_code] - if self.keyboard == []: # if no keyboard defined, then use available chars - # keyboard of ten characters per row - for i, c in enumerate(self.characters): - if i % 10 == 0: - self.keyboard.append([]) - self.keyboard[-1].append(c) - self.keyboard[-1].insert(0, "⇨") - self.keyboard[-1].append("⌫") - - # Deal with bottom row being too crammed: - if len(self.keyboard[-1]) == 11: - popped_c = self.keyboard[-1].pop(1) - self.keyboard[-2].insert(-1, popped_c) - if len(self.keyboard[-1]) == 12: - popped_c = self.keyboard[-2].pop(0) - self.keyboard[-3].insert(-1, popped_c) - popped_c = self.keyboard[-1].pop(2) - self.keyboard[-2].insert(-1, popped_c) - popped_c = self.keyboard[-1].pop(2) - self.keyboard[-2].insert(-1, popped_c) + keyboard_config = keyboards.get(language_code, {"default": None, "layouts": {}}) + self.keyboard_layouts = self._build_keyboard_layouts(keyboard_config) + self.keyboard_layout_name = self._select_keyboard_layout( + keyboard_layout, keyboard_config.get("default") + ) + layout_meta = self.keyboard_layouts[self.keyboard_layout_name] + self.keyboard_layout_label = layout_meta["label"] + self.keyboard = layout_meta["rows"] + + def _build_keyboard_layouts(self, keyboard_config): + layouts = {} + for layout_name, layout_meta in keyboard_config.get("layouts", {}).items(): + label = layout_meta.get("label") or layout_name.replace("_", " ").title() + rows = layout_meta.get("rows", []) + layouts[layout_name] = {"label": label, "rows": rows} + + if not layouts: + layouts["alphabetical"] = { + "label": "Alphabetical", + "rows": self._generate_alphabetical_keyboard(), + } + return layouts + + def _select_keyboard_layout(self, requested_layout, default_layout): + if requested_layout and requested_layout in self.keyboard_layouts: + return requested_layout + if default_layout and default_layout in self.keyboard_layouts: + return default_layout + return next(iter(self.keyboard_layouts)) + + def _generate_alphabetical_keyboard(self): + keyboard = [] + for i, c in enumerate(self.characters): + if i % 10 == 0: + keyboard.append([]) + keyboard[-1].append(c) + if not keyboard: + return keyboard + keyboard[-1].insert(0, "⇨") + keyboard[-1].append("⌫") + + # Deal with bottom row being too crammed: + if len(keyboard) >= 2 and len(keyboard[-1]) == 11: + popped_c = keyboard[-1].pop(1) + keyboard[-2].insert(-1, popped_c) + if len(keyboard) >= 3 and len(keyboard[-1]) == 12: + popped_c = keyboard[-2].pop(0) + keyboard[-3].insert(-1, popped_c) + popped_c = keyboard[-1].pop(2) + keyboard[-2].insert(-1, popped_c) + popped_c = keyboard[-1].pop(2) + keyboard[-2].insert(-1, popped_c) + return keyboard ############################################################################### @@ -292,8 +361,19 @@ def language(lang_code): if lang_code not in language_codes: return "Language not found" word_list = language_codes_5words[lang_code] - language = Language(lang_code, word_list) - return render_template("game.html", language=language) + cookie_key = f"keyboard_layout_{lang_code}" + requested_layout = request.args.get("layout") or request.cookies.get(cookie_key) + language = Language(lang_code, word_list, requested_layout) + response = make_response(render_template("game.html", language=language)) + selected_layout = language.keyboard_layout_name + if request.cookies.get(cookie_key) != selected_layout: + response.set_cookie( + cookie_key, + selected_layout, + max_age=60 * 60 * 24 * 365, + samesite="Lax", + ) + return response if __name__ == "__main__": diff --git a/webapp/data/languages/en/en_keyboard.json b/webapp/data/languages/en/en_keyboard.json index 3112200d..1632c078 100644 --- a/webapp/data/languages/en/en_keyboard.json +++ b/webapp/data/languages/en/en_keyboard.json @@ -1,36 +1,122 @@ -[ - [ - "q", - "w", - "e", - "r", - "t", - "y", - "u", - "i", - "o", - "p" - ], - [ - "a", - "s", - "d", - "f", - "g", - "h", - "j", - "k", - "l" - ], - [ - "⇨", - "z", - "x", - "c", - "v", - "b", - "n", - "m", - "⌫" - ] -] \ No newline at end of file +{ + "default": "qwerty", + "layouts": { + "qwerty": { + "label": "QWERTY", + "rows": [ + [ + "q", + "w", + "e", + "r", + "t", + "y", + "u", + "i", + "o", + "p" + ], + [ + "a", + "s", + "d", + "f", + "g", + "h", + "j", + "k", + "l" + ], + [ + "⇨", + "z", + "x", + "c", + "v", + "b", + "n", + "m", + "⌫" + ] + ] + }, + "dvorak": { + "label": "Dvorak", + "rows": [ + [ + "p", + "y", + "f", + "g", + "c", + "r", + "l" + ], + [ + "a", + "o", + "e", + "u", + "i", + "d", + "h", + "t", + "n", + "s" + ], + [ + "⇨", + "q", + "j", + "k", + "x", + "b", + "m", + "w", + "v", + "z", + "⌫" + ] + ] + }, + "alphabetical": { + "label": "Alphabetical", + "rows": [ + [ + "a", + "b", + "c", + "d", + "e", + "f", + "g", + "h", + "i", + "j" + ], + [ + "k", + "l", + "m", + "n", + "o", + "p", + "q", + "r", + "s", + "t" + ], + [ + "⇨", + "u", + "v", + "w", + "x", + "y", + "z", + "⌫" + ] + ] + } + } +} diff --git a/webapp/data/languages/he/he_keyboard.json b/webapp/data/languages/he/he_keyboard.json index 0637a088..00b5ab71 100644 --- a/webapp/data/languages/he/he_keyboard.json +++ b/webapp/data/languages/he/he_keyboard.json @@ -1 +1,85 @@ -[] \ No newline at end of file +{ + "default": "alphabetical", + "layouts": { + "alphabetical": { + "label": "Alphabetical", + "rows": [ + [ + "א", + "ב", + "ג", + "ד", + "ה", + "ו", + "ז", + "ח", + "ט" + ], + [ + "י", + "כ", + "ך", + "ל", + "מ", + "ם", + "נ", + "ן", + "ס" + ], + [ + "⇨", + "ע", + "פ", + "ף", + "צ", + "ץ", + "ק", + "ר", + "ש", + "ת", + "⌫" + ] + ] + }, + "hebrew_qwerty": { + "label": "Hebrew QWERTY", + "rows": [ + [ + "ק", + "ר", + "א", + "ט", + "ו", + "ן", + "ם", + "פ", + "ף" + ], + [ + "ש", + "ד", + "ג", + "כ", + "ך", + "ע", + "י", + "ח", + "ל" + ], + [ + "⇨", + "ז", + "ס", + "ב", + "ה", + "נ", + "מ", + "צ", + "ץ", + "ת", + "⌫" + ] + ] + } + } +} diff --git a/webapp/templates/game.html b/webapp/templates/game.html index 917d36af..3f5022a2 100644 --- a/webapp/templates/game.html +++ b/webapp/templates/game.html @@ -265,6 +265,24 @@
Allow any word (easy mode)
@@ -372,4 +390,4 @@