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 @@

SETTINGS

+ {% if language.keyboard_layouts|length > 1 %} +
+
+

Keyboard layout

+ +
+ {% endif %} +

Allow any word (easy mode)

@@ -372,4 +390,4 @@

Wordle {{ - \ No newline at end of file + From 4287c948f6358e36ab50f77d0a20a370766104f3 Mon Sep 17 00:00:00 2001 From: Xavier <0xavier0@gmail.com> Date: Sat, 15 Nov 2025 08:31:24 -0500 Subject: [PATCH 2/4] Refactor he_keyboard.json for improved formatting and readability --- webapp/data/languages/he/he_keyboard.json | 108 ++++++---------------- 1 file changed, 26 insertions(+), 82 deletions(-) diff --git a/webapp/data/languages/he/he_keyboard.json b/webapp/data/languages/he/he_keyboard.json index 00b5ab71..792cd053 100644 --- a/webapp/data/languages/he/he_keyboard.json +++ b/webapp/data/languages/he/he_keyboard.json @@ -1,85 +1,29 @@ { - "default": "alphabetical", - "layouts": { - "alphabetical": { - "label": "Alphabetical", - "rows": [ - [ - "א", - "ב", - "ג", - "ד", - "ה", - "ו", - "ז", - "ח", - "ט" - ], - [ - "י", - "כ", - "ך", - "ל", - "מ", - "ם", - "נ", - "ן", - "ס" - ], - [ - "⇨", - "ע", - "פ", - "ף", - "צ", - "ץ", - "ק", - "ר", - "ש", - "ת", - "⌫" - ] - ] - }, - "hebrew_qwerty": { - "label": "Hebrew QWERTY", - "rows": [ - [ - "ק", - "ר", - "א", - "ט", - "ו", - "ן", - "ם", - "פ", - "ף" - ], - [ - "ש", - "ד", - "ג", - "כ", - "ך", - "ע", - "י", - "ח", - "ל" - ], - [ - "⇨", - "ז", - "ס", - "ב", - "ה", - "נ", - "מ", - "צ", - "ץ", - "ת", - "⌫" - ] - ] - } + "default": "alphabetical", + "layouts": { + "alphabetical": { + "label": "Alphabetical", + "rows": [ + ["א", "ב", "ג", "ד", "ה", "ו", "ז", "ח", "ט"], + ["י", "כ", "ך", "ל", "מ", "ם", "נ", "ן", "ס"], + ["⇨", "ע", "פ", "ף", "צ", "ץ", "ק", "ר", "ש", "ת", "⌫"] + ] + }, + "hebrew_qwerty": { + "label": "Hebrew QWERTY", + "rows": [ + ["ק", "ר", "א", "ט", "ו", "ן", "ם", "פ", "ף"], + ["ש", "ד", "ג", "כ", "ך", "ע", "י", "ח", "ל"], + ["⇨", "ז", "ס", "ב", "ה", "נ", "מ", "צ", "ץ", "ת", "⌫"] + ] + }, + "ken_custom": { + "label": "Ken Custom", + "rows": [ + ["ק", "ר", "א", "ט", "ו", "ן", "ם", "פ"], + ["ש", "ד", "ג", "כ", "ע", "י", "ח", "ל", "ך", "ף"], + ["⇨", "ז", "ס", "ב", "ה", "נ", "מ", "צ", "ת", "ץ", "⌫"] + ] } + } } From 5e608c8a8f742d5bd655358d880025e1978c798f Mon Sep 17 00:00:00 2001 From: Xavier <0xavier0@gmail.com> Date: Sat, 15 Nov 2025 12:21:56 -0500 Subject: [PATCH 3/4] Refactor he_keyboard.json to remove redundant hebrew_qwerty layout --- webapp/data/languages/he/he_keyboard.json | 8 -------- 1 file changed, 8 deletions(-) diff --git a/webapp/data/languages/he/he_keyboard.json b/webapp/data/languages/he/he_keyboard.json index 792cd053..dafdce80 100644 --- a/webapp/data/languages/he/he_keyboard.json +++ b/webapp/data/languages/he/he_keyboard.json @@ -10,14 +10,6 @@ ] }, "hebrew_qwerty": { - "label": "Hebrew QWERTY", - "rows": [ - ["ק", "ר", "א", "ט", "ו", "ן", "ם", "פ", "ף"], - ["ש", "ד", "ג", "כ", "ך", "ע", "י", "ח", "ל"], - ["⇨", "ז", "ס", "ב", "ה", "נ", "מ", "צ", "ץ", "ת", "⌫"] - ] - }, - "ken_custom": { "label": "Ken Custom", "rows": [ ["ק", "ר", "א", "ט", "ו", "ן", "ם", "פ"], From 029ca9d5a43b368ee8b11d39451d5b9b7f1fc0ba Mon Sep 17 00:00:00 2001 From: Xavier <0xavier0@gmail.com> Date: Sat, 15 Nov 2025 22:30:10 -0500 Subject: [PATCH 4/4] Enhance documentation for keyboard layout functions and improve accessibility in game.html --- webapp/app.py | 26 ++++++++++++++++++++++++++ webapp/templates/game.html | 4 ++-- 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/webapp/app.py b/webapp/app.py index d7223704..115c8ae4 100644 --- a/webapp/app.py +++ b/webapp/app.py @@ -116,6 +116,12 @@ def load_language_config(lang): def load_keyboard(lang): + """ + Return normalized keyboard layouts for a language. + + Reads the language-specific keyboard JSON, handles legacy formats, + and always returns a dict with default layout metadata. + """ keyboard_path = f"{data_dir}languages/{lang}/{lang}_keyboard.json" try: with open(keyboard_path, "r") as f: @@ -237,6 +243,14 @@ class Language: """Holds the attributes of a language""" def __init__(self, language_code, word_list, keyboard_layout=None): + """ + Populate language metadata and select the desired keyboard layout. + + Parameters: + language_code: Two-letter language identifier (e.g. 'en'). + word_list: Base five-letter word list for the language. + keyboard_layout: Optional layout name chosen via query/cookie. + """ self.language_code = language_code self.word_list = word_list self.word_list_supplement = language_codes_5words_supplements[language_code] @@ -263,6 +277,12 @@ def __init__(self, language_code, word_list, keyboard_layout=None): self.keyboard = layout_meta["rows"] def _build_keyboard_layouts(self, keyboard_config): + """ + Build canonical layout metadata from the raw keyboard config. + + Ensures every layout has a label/rows and falls back to an + auto-generated alphabetical layout when no layouts are provided. + """ layouts = {} for layout_name, layout_meta in keyboard_config.get("layouts", {}).items(): label = layout_meta.get("label") or layout_name.replace("_", " ").title() @@ -277,6 +297,12 @@ def _build_keyboard_layouts(self, keyboard_config): return layouts def _select_keyboard_layout(self, requested_layout, default_layout): + """ + Choose the active keyboard layout in priority order. + + Prefers the user-requested layout, then the config default, + and finally falls back to the first available layout. + """ if requested_layout and requested_layout in self.keyboard_layouts: return requested_layout if default_layout and default_layout in self.keyboard_layouts: diff --git a/webapp/templates/game.html b/webapp/templates/game.html index 3f5022a2..9408a66c 100644 --- a/webapp/templates/game.html +++ b/webapp/templates/game.html @@ -269,8 +269,8 @@

SETTINGS

-

Keyboard layout

- {% for layout_name, layout_meta in language.keyboard_layouts.items() %}