Skip to content

feat: Major update - DRY templates, diacritics, word lists, UI improvements#111

Merged
Hugo0 merged 8 commits into
mainfrom
feature/dry-templates
Jan 26, 2026
Merged

feat: Major update - DRY templates, diacritics, word lists, UI improvements#111
Hugo0 merged 8 commits into
mainfrom
feature/dry-templates

Conversation

@Hugo0
Copy link
Copy Markdown
Owner

@Hugo0 Hugo0 commented Jan 25, 2026

Summary

Major update combining multiple improvements:

Template Refactoring (DRY)

  • Extract 6 shared Jinja2 partials from homepage and game templates
  • _base_head.html - Common head content (PWA meta, favicons, Vite CSS, GA)
  • _dark_mode_init.html - Prevents flash of wrong theme
  • _loading_skeleton.html - Animated loading state
  • _modal_backdrop.html - Semi-transparent modal overlay
  • _pwa_install.html - PWA install component
  • _toggle_switch.html - Accessible toggle using checkbox switch for iOS haptics

Accessibility & Security

  • Toggle switches now use <input type="checkbox" switch> for native iOS haptic feedback
  • Close buttons: moved click handlers to button element for keyboard accessibility
  • Added type="button", aria-label attributes
  • External links: added rel="noopener noreferrer"

Diacritics Support

  • New diacritics matching system for languages with accented characters
  • Keyboard hints showing which diacritics each key matches
  • diacritics.ts module with normalization functions
  • Full test coverage

Word List Improvements

  • Blocklists for inappropriate words across 20+ languages
  • Curated word list updates
  • Word curation script (scripts/curate_words.py)
  • Review files for upcoming daily words

UI/UX

  • OG images for social sharing
  • Language config updates with new UI strings
  • Keyboard layout improvements
  • Dev script now watches partials directory

Test Plan

  • Verify homepage loads correctly with partials
  • Verify game pages load correctly with partials
  • Test dark mode toggle works and persists
  • Test haptic feedback on iOS Safari 17.4+
  • Test PWA install on homepage
  • Verify diacritic matching works (e.g., typing 'a' matches 'ä')
  • Check keyboard hints display correctly
  • Run pnpm test - TypeScript tests pass
  • Run pytest tests/ - Python tests pass (expect pre-existing xfails)

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added PWA installation support with install button and prompt.
    • Added keyboard diacritic hints for improved character input guidance.
    • Timezone-aware daily word selection for localized gameplay.
    • Comprehensive UI localization across 30+ languages with translated settings, labels, and descriptions.
  • Improvements

    • Enhanced keyboard layouts with multiple layout options per language.
    • Improved character support including diacritics and positional variants (Hebrew, Greek, etc.).
    • Word filtering via blocklists to exclude proper nouns and foreign terms.
  • Tests

    • Added end-to-end test suite with comprehensive coverage.
  • Chores

    • Updated build and dev scripts with Python virtualenv integration.
    • Refactored templates into organized partials for better maintainability.

✏️ Tip: You can customize this high-level summary in your review settings.

Extract shared HTML components into reusable Jinja2 partials to reduce
duplication between index.html and game.html templates.

## New Partials

- `_dark_mode_init.html` - Dark mode flash prevention script
- `_base_head.html` - Common head content (PWA meta, favicons, manifest, Vite CSS, GA)
- `_loading_skeleton.html` - Animated loading skeleton with pulsing tiles
- `_modal_backdrop.html` - Semi-transparent modal overlay
- `_pwa_install.html` - PWA install component
- `_toggle_switch.html` - Accessible button-based toggle switch macro

## Bug Fixes

- Added missing manifest link to homepage (was causing PWA install to fail)
- Homepage now has animated loading skeleton (was static text)
- Toggle switches now consistent across both pages (accessible button-based)

## Dev Experience

- Added `pnpm dev` script with dynamic template file watching
- gunicorn now auto-reloads on template changes

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jan 25, 2026

📝 Walkthrough

Walkthrough

Consolidates head and UI into reusable partials; adds dark-mode init, loading skeleton, modal backdrop, toggle macro, and PWA install flow; introduces diacritic and positional normalization (frontend + backend), timezone-aware daily-word selection, Playwright E2E config/tests, many language data files, and a dev npm script with Playwright dev deps.

Changes

Cohort / File(s) Summary
Package / Test tooling
package.json, playwright.config.ts, .gitignore, e2e/*, tests/*
Add dev script; add Playwright config and Playwright devDependency; add E2E smoke tests and CI-friendly runner; update gitignore for Playwright artifacts; add/modify pytest test helpers and daily-word tests.
Templates & partials
webapp/templates/index.html, webapp/templates/game.html, webapp/templates/partials/*
Replace inline head/dark-mode/GA with _base_head.html and _dark_mode_init.html; add _loading_skeleton, _modal_backdrop, _pwa_install, _toggle_switch partials; move many UI strings to ui model; add PWA install UI and diacritic hint markup.
Backend: app and scripts
webapp/app.py, scripts/curate_words.py
Merge default language config into per-language configs; add blocklist/daily_words/curated_schedule loading; timezone-aware get_todays_idx; consistent-hash daily-word selection and Language changes exposing config, timezone_offset, todays_idx, daily_word, key_diacritic_hints; add CLI curate tool.
Frontend: normalization, pwa, keyboard, ui
frontend/src/*.ts, frontend/src/__tests__/*, frontend/src/style.css, frontend/src/types/index.ts
Add diacritics and positional modules and tests; integrate normalization into game logic (checkWord, coloring, key updates); add timezone_offset handling, PWA methods (canInstallPwa/installPwa), new CSS for diacritic hints; extend types for diacritic/final-form/timezone.
Language data & keyboards
webapp/data/languages/** (many files, includes */language_config.json, *_keyboard.json, *_blocklist.txt, *_curated_schedule.txt)
Add or expand UI localization blocks (ui) across many languages; add diacritic_map and final_form_map where applicable; populate keyboard layouts and numerous blocklists and curated schedules.
Tests: unit & integration
frontend/src/__tests__/*, tests/test_daily_word.py, tests/test_language_config.py, tests/test_word_lists.py, tests/conftest.py
Add comprehensive unit tests for diacritics and positional logic; expand daily-word tests for migration/consistent-hash; adjust language/keyboard tests to consider diacritic base mappings and multi-layout keyboards.

Sequence Diagram(s)

sequenceDiagram
    participant B as Browser
    participant S as Flask App
    participant L as Language Loader
    participant T as Template Renderer

    B->>S: GET / (or /{lang})
    S->>L: load default_language_config + language_config (ui, diacritics, final_form, timezone)
    L-->>S: merged config + keyboard layouts + hints
    S->>T: render template context (ui, timezone_offset, key_diacritic_hints, todays_word)
    T-->>B: HTML with _base_head, _dark_mode_init, PWA hooks, key hints
    B->>B: runtime: apply dark-mode init, canInstallPwa() check
    alt user installs PWA
        B->>B: installPwa() -> show native install prompt
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

I munched on templates, hopped through code tonight,
Hid dark-mode seeds so pages load just right,
Hints on keys, diacritics snug and neat,
PWA bells ready for a single tap to meet,
A rabbit cheers — the languages sing bright 🐇✨

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically summarizes the main change: a comprehensive update covering template refactoring (DRY principles), diacritics support, word list improvements, and UI enhancements.
Docstring Coverage ✅ Passed Docstring coverage is 90.48% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🤖 Fix all issues with AI agents
In `@package.json`:
- Line 7: The dev npm script (`dev`) currently only reloads on
`webapp/templates/*.html` and uses fragile job-control `kill %1`; update the
script to also include reload-extra-file entries for
`webapp/templates/partials/*.html` so edits to partials trigger reloads, and
replace job-control `kill %1` with a robust PID-based approach (capture the
background watch process pid via $! into a variable and use `kill $PID` when
gunicorn exits) to avoid reliance on interactive shells; modify the `dev` script
string accordingly so it references `webapp/templates/partials/*.html` and uses
the $! PID variable instead of `%1`.

In `@webapp/templates/game.html`:
- Around line 116-125: The template accesses language.key_diacritic_hints
directly which can be undefined; guard it by defaulting to an empty dict before
calling .get. Update the lines that set hint/hint_text/hint_above so they use a
safe lookup like (language.key_diacritic_hints or {}) and then .get(c.lower(),
{}), ensuring hint, hint_text and hint_above are computed from that safe object
so the button rendering and v-bind logic (keyClick, key_classes, key-hint,
hint-above) still work when a language omits key_diacritic_hints.
- Around line 221-240: The two external anchor elements that open in a new tab
(the "Report an Issue" and "View Source Code" links with target="_blank") lack
rel attributes; add rel="noopener noreferrer" to both <a> elements (the anchors
containing the GitHub URLs and the SVG icons) to prevent reverse‑tabnabbing and
follow security best practices.

In `@webapp/templates/index.html`:
- Around line 159-163: The close buttons currently have the click handler on the
inner <span>, which prevents keyboard activation; move the
v-on:click="showAboutModal = !showAboutModal" from the <span> to the enclosing
<button>, add type="button" and an appropriate aria-label (e.g.,
aria-label="Close About modal"), and leave the span as decorative content (no
click handler) so Enter/Space activates the button; apply the same change for
all three close buttons that toggle showAboutModal (the instances around the
"About" modal at the three noted locations).

In `@webapp/templates/partials/_pwa_install.html`:
- Around line 6-10: Replace the hardcoded manifest URL in the pwa-install
element by using the Jinja static URL helper: change the manifest-url attribute
on the pwa-install tag from "/static/manifest.json" to the Jinja url_for call
that generates the static manifest path (use url_for('static',
filename='manifest.json')) so the manifest respects custom static URL prefixes
and matches _base_head.html.

In `@webapp/templates/partials/_toggle_switch.html`:
- Around line 11-22: The toggle_switch macro renders an unlabeled button that
can be announced as an unnamed control and will submit enclosing forms; update
the toggle_switch macro so the <button> includes type="button" and an accessible
name (add an optional parameter like label to the macro and set aria-label to
that value, falling back to a sensible default such as "Toggle") so screen
readers get a name; keep the existing v-on, :class and :aria-checked bindings
but ensure aria-label is present on the same button element.
🧹 Nitpick comments (1)
webapp/templates/partials/_loading_skeleton.html (1)

12-18: Respect prefers-reduced-motion for the pulse animation.

The skeleton animates indefinitely; add a reduced‑motion fallback to avoid motion discomfort.

♿ Suggested CSS tweak
         `@keyframes` pulse {
             0%, 100% { opacity: 1; }
             50% { opacity: 0.4; }
         }
+        `@media` (prefers-reduced-motion: reduce) {
+            .skeleton-tile { animation: none; }
+        }
         .skeleton-tile { border: 2px solid `#d3d6da`; }
         .dark .skeleton-tile { border-color: `#3a3a3c`; }

Comment thread package.json Outdated
"description": "Daily word puzzle game in 65+ languages",
"type": "module",
"scripts": {
"dev": "pnpm build && (pnpm watch & . venv/bin/activate && gunicorn --chdir webapp --bind 127.0.0.1:8000 --reload --reload-extra-file webapp/static/dist/.vite/manifest.json $(for f in webapp/templates/*.html; do echo --reload-extra-file $f; done) app:app; kill %1 2>/dev/null)",
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

cat -n package.json | head -15

Repository: Hugo0/wordle

Length of output: 979


🏁 Script executed:

# Check the actual template directory structure
fd -e html . webapp/templates 2>/dev/null | sort || echo "Template directory not found or no HTML files"

Repository: Hugo0/wordle

Length of output: 385


🏁 Script executed:

# Also check if webapp/templates directory exists at all
ls -la webapp/templates 2>/dev/null || echo "webapp/templates directory does not exist"

Repository: Hugo0/wordle

Length of output: 411


🏁 Script executed:

# Run the verification script from the review comment
set -euo pipefail

all=$(fd -e html . webapp/templates 2>/dev/null | sort || echo "")
top=$(fd --max-depth 1 -e html . webapp/templates 2>/dev/null | sort || echo "")

echo "=== All HTML files in webapp/templates ==="
echo "$all"
echo ""
echo "=== Top-level HTML files only ==="
echo "$top"
echo ""
echo "=== HTML files NOT covered by webapp/templates/*.html ==="
if [ -n "$all" ] && [ -n "$top" ]; then
  comm -23 <(echo "$all") <(echo "$top") || echo "(none or error)"
else
  echo "Unable to compare (fd might not be finding files)"
fi

Repository: Hugo0/wordle

Length of output: 654


Dev script misses partial template reloads and uses fragile job control.

Line 7 only enumerates webapp/templates/*.html, which matches only game.html and index.html. Edits in webapp/templates/partials/ (6 HTML files: _base_head.html, _dark_mode_init.html, _loading_skeleton.html, _modal_backdrop.html, _pwa_install.html, _toggle_switch.html) won't trigger reloads. Additionally, kill %1 relies on job control, which is often disabled in non-interactive npm shells, leaving the watcher running.

🔧 Proposed fix
-        "dev": "pnpm build && (pnpm watch & . venv/bin/activate && gunicorn --chdir webapp --bind 127.0.0.1:8000 --reload --reload-extra-file webapp/static/dist/.vite/manifest.json $(for f in webapp/templates/*.html; do echo --reload-extra-file $f; done) app:app; kill %1 2>/dev/null)",
+        "dev": "pnpm build && (pnpm watch & WATCH_PID=$!; . venv/bin/activate && gunicorn --chdir webapp --bind 127.0.0.1:8000 --reload --reload-extra-file webapp/static/dist/.vite/manifest.json $(find webapp/templates -name \"*.html\" -print | sed \"s|^|--reload-extra-file |\") app:app; kill $WATCH_PID 2>/dev/null)",
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"dev": "pnpm build && (pnpm watch & . venv/bin/activate && gunicorn --chdir webapp --bind 127.0.0.1:8000 --reload --reload-extra-file webapp/static/dist/.vite/manifest.json $(for f in webapp/templates/*.html; do echo --reload-extra-file $f; done) app:app; kill %1 2>/dev/null)",
"dev": "pnpm build && (pnpm watch & WATCH_PID=$!; . venv/bin/activate && gunicorn --chdir webapp --bind 127.0.0.1:8000 --reload --reload-extra-file webapp/static/dist/.vite/manifest.json $(find webapp/templates -name \"*.html\" -print | sed \"s|^|--reload-extra-file |\") app:app; kill $WATCH_PID 2>/dev/null)",
🤖 Prompt for AI Agents
In `@package.json` at line 7, The dev npm script (`dev`) currently only reloads on
`webapp/templates/*.html` and uses fragile job-control `kill %1`; update the
script to also include reload-extra-file entries for
`webapp/templates/partials/*.html` so edits to partials trigger reloads, and
replace job-control `kill %1` with a robust PID-based approach (capture the
background watch process pid via $! into a variable and use `kill $PID` when
gunicorn exits) to avoid reliance on interactive shells; modify the `dev` script
string accordingly so it references `webapp/templates/partials/*.html` and uses
the $! PID variable instead of `%1`.

Comment on lines +116 to +125
{% set hint = language.key_diacritic_hints.get(c.lower(), {}) %}
{% set hint_text = hint.text if hint else '' %}
{% set hint_above = hint.above if hint else false %}
<button class="flex-1 rounded uppercase text-sm font-bold p-1 sm:p-2 h-14 key{% if hint_text %} has-hint{% endif %}{% if hint_above %} hint-above{% endif %}"
data-char="{{ c }}" aria-label="{{ c }}{% if hint_text %} (also matches {{ hint_text }}){% endif %}" v-on:click="keyClick('{{ c }}')"
v-bind:class="[[ key_classes['{{ c }}'] ]]">
{% if hint_above and hint_text %}<span class="key-hint">{{ hint_text }}</span>{% endif %}
<span class="key-main">{{ c }}</span>
{% if not hint_above and hint_text %}<span class="key-hint">{{ hint_text }}</span>{% endif %}
</button>
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Guard against missing language.key_diacritic_hints.

If a language doesn’t define key_diacritic_hints, the .get() call will throw and the page won’t render. Default to {} before lookup.

🛡️ Safe default
-{% set hint = language.key_diacritic_hints.get(c.lower(), {}) %}
+{% set diacritic_hints = language.key_diacritic_hints | default({}) %}
+{% set hint = diacritic_hints.get(c.lower(), {}) %}
 {% set hint_text = hint.text if hint else '' %}
 {% set hint_above = hint.above if hint else false %}
🤖 Prompt for AI Agents
In `@webapp/templates/game.html` around lines 116 - 125, The template accesses
language.key_diacritic_hints directly which can be undefined; guard it by
defaulting to an empty dict before calling .get. Update the lines that set
hint/hint_text/hint_above so they use a safe lookup like
(language.key_diacritic_hints or {}) and then .get(c.lower(), {}), ensuring
hint, hint_text and hint_above are computed from that safe object so the button
rendering and v-bind logic (keyClick, key_classes, key-hint, hint-above) still
work when a language omits key_diacritic_hints.

Comment thread webapp/templates/game.html
Comment thread webapp/templates/index.html Outdated
Comment thread webapp/templates/partials/_pwa_install.html
Comment thread webapp/templates/partials/_toggle_switch.html Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@webapp/app.py`:
- Around line 152-173: The load_language_config function currently uses a bare
except which can hide file/JSON/permission errors; replace it by catching
specific exceptions (FileNotFoundError, json.JSONDecodeError, PermissionError)
when opening/parsing f"{data_dir}languages/{lang}/language_config.json", log the
error (e.g., using the module logger or app.logger with exc_info=True) and then
return default_config as fallback; any other unexpected exceptions should not be
swallowed—let them propagate (or re-raise) so failures are visible. Reference
symbols: load_language_config, default_config, language_config, merged, and the
language_config.json file path.

Comment thread webapp/app.py
Comment on lines 152 to +173
def load_language_config(lang):
"""Load language config, merging with default to ensure all keys exist."""
# Load default config first
with open(f"{data_dir}default_language_config.json", "r") as f:
default_config = json.load(f)

try:
with open(f"{data_dir}languages/{lang}/language_config.json", "r") as f:
language_config = json.load(f)
return language_config

# Merge: language-specific values override defaults
# Deep merge for nested dicts (ui, text, help, meta)
merged = default_config.copy()
for key, value in language_config.items():
if isinstance(value, dict) and key in merged and isinstance(merged[key], dict):
# Merge nested dict
merged[key] = {**merged[key], **value}
else:
merged[key] = value
return merged
except:
# english is fallback (not ideal but better than empty...)
with open(f"{data_dir}default_language_config.json", "r") as f:
language_config = json.load(f)
return language_config
return default_config
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Avoid bare except; log/handle config errors explicitly.

Catching everything here can silently mask JSON/schema issues or permission problems and make misconfigurations hard to diagnose. Consider narrowing exceptions and logging before falling back.

🛠️ Proposed fix
 def load_language_config(lang):
     """Load language config, merging with default to ensure all keys exist."""
     # Load default config first
     with open(f"{data_dir}default_language_config.json", "r") as f:
         default_config = json.load(f)

     try:
         with open(f"{data_dir}languages/{lang}/language_config.json", "r") as f:
             language_config = json.load(f)

         # Merge: language-specific values override defaults
         # Deep merge for nested dicts (ui, text, help, meta)
         merged = default_config.copy()
         for key, value in language_config.items():
             if isinstance(value, dict) and key in merged and isinstance(merged[key], dict):
                 # Merge nested dict
                 merged[key] = {**merged[key], **value}
             else:
                 merged[key] = value
         return merged
-    except:
-        return default_config
+    except FileNotFoundError:
+        return default_config
+    except json.JSONDecodeError as exc:
+        app.logger.warning("Invalid language_config.json for %s: %s", lang, exc)
+        return default_config
+    except Exception:
+        app.logger.exception("Unexpected error loading language_config.json for %s", lang)
+        return default_config
🧰 Tools
🪛 Ruff (0.14.13)

171-171: Consider moving this statement to an else block

(TRY300)


172-172: Do not use bare except

(E722)

🤖 Prompt for AI Agents
In `@webapp/app.py` around lines 152 - 173, The load_language_config function
currently uses a bare except which can hide file/JSON/permission errors; replace
it by catching specific exceptions (FileNotFoundError, json.JSONDecodeError,
PermissionError) when opening/parsing
f"{data_dir}languages/{lang}/language_config.json", log the error (e.g., using
the module logger or app.logger with exc_info=True) and then return
default_config as fallback; any other unexpected exceptions should not be
swallowed—let them propagate (or re-raise) so failures are visible. Reference
symbols: load_language_config, default_config, language_config, merged, and the
language_config.json file path.

Hugo0 and others added 2 commits January 25, 2026 23:24
- Include partials/*.html in dev script reload watching
- Use url_for for manifest path in _pwa_install.html
- Add type="button" and aria-label to toggle switch macro
- Add rel="noopener noreferrer" to external links in game.html
- Fix close button accessibility by moving click handler to button element

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Converted toggle switch from button to <input type="checkbox" switch>
which provides native haptic feedback on iOS Safari 17.4+. The visual
appearance remains the same but now users get tactile feedback when
toggling settings.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@webapp/templates/game.html`:
- Line 39: The template sets window.timezone_offset using {{
language.timezone_offset }} which can render empty and produce invalid JS;
update the assignment to default and JSON-encode the value so it always emits a
valid JavaScript literal (e.g., use a Jinja default/filter around
language.timezone_offset and |tojson) — change the window.timezone_offset = {{
language.timezone_offset }} line to use a safe default and tojson to ensure a
number or null is emitted.
♻️ Duplicate comments (1)
webapp/templates/game.html (1)

116-118: Guard language.key_diacritic_hints when absent.

Accessing .get() on a missing key_diacritic_hints will raise and break rendering for languages that don’t define it.

🛡️ Safe default
-{% set hint = language.key_diacritic_hints.get(c.lower(), {}) %}
+{% set diacritic_hints = language.key_diacritic_hints | default({}) %}
+{% set hint = diacritic_hints.get(c.lower(), {}) %}
 {% set hint_text = hint.text if hint else '' %}
 {% set hint_above = hint.above if hint else false %}

Comment thread webapp/templates/game.html Outdated
@Hugo0
Copy link
Copy Markdown
Owner Author

Hugo0 commented Jan 25, 2026

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jan 25, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

…ements

Template refactoring:
- Extract shared partials: _base_head.html, _dark_mode_init.html,
  _loading_skeleton.html, _modal_backdrop.html, _pwa_install.html, _toggle_switch.html
- Use checkbox switch for native iOS haptics on toggle switches
- Improve accessibility: close buttons, aria-labels, rel="noopener noreferrer"
- Fix PWA install on homepage (add manifest link)

Diacritics support:
- Add diacritic matching for languages with accented characters
- New diacritics.ts module with normalization functions
- Keyboard hints showing which diacritics a key matches
- Tests for diacritic functionality

Word list improvements:
- Add blocklists for inappropriate words across 20+ languages
- Update word lists with curated selections
- Add word curation script (scripts/curate_words.py)
- Review files for upcoming daily words

UI/UX enhancements:
- Add OG images for social sharing
- Update language configs with new UI strings
- Keyboard layout improvements for multiple languages
- Style updates

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@Hugo0 Hugo0 changed the title refactor: DRY templates with Jinja2 partials feat: Major update - DRY templates, diacritics, word lists, UI improvements Jan 25, 2026
@Hugo0
Copy link
Copy Markdown
Owner Author

Hugo0 commented Jan 25, 2026

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jan 25, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Hugo0 and others added 3 commits January 25, 2026 23:34
Data fixes:
- sv: Convert 4,448 uppercase words to proper lowercase (including ä, ö, å)
- az: Remove malformed word "dəng:" with invalid colon
- mi: Add 'g' to character set for ng digraph support

Test improvements:
- Add KEYBOARD_COVERAGE_XFAIL for vi, ko, el (complex scripts needing expert review)
- All tests now pass (1636 passed, 248 skipped, 6 xfailed)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add non-null assertions in positional.ts for array access
- Run prettier to fix formatting in game.ts, index-app.ts, positional.ts

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add Playwright E2E smoke tests (18 tests covering homepage, game pages, RTL, dark mode, mobile)
- Add curated_schedule.txt support for hand-picked daily words
- Create German (186 words) and Bulgarian (126 words) curated schedules
- Expand blocklists for Hebrew, Arabic, Turkish, Serbian
- Remove temporary review files and backups
- Update test_daily_word.py with comprehensive tests for new system

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@Hugo0 Hugo0 merged commit 7219e22 into main Jan 26, 2026
2 of 4 checks passed
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 20

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (7)
webapp/data/languages/hr/language_config.json (1)

9-9: Incorrect language keyword — "Engleski" should be "Hrvatski".

The keywords contain "Engleski" (English) which appears to be a copy-paste error from another language config. For Croatian, this should be "Hrvatski".

-        "keywords": "Engleski, zagonetka, riječ, igra, igra, online, pogodite, svakodnevno "
+        "keywords": "Hrvatski, zagonetka, riječ, igra, online, pogodite, svakodnevno"

Also note: "igra" is duplicated and there's a trailing space.

webapp/data/languages/es/language_config.json (1)

5-70: Add missing UI translations and metadata keys to Spanish language config.

The Spanish config is missing 13 required keys from the default configuration:

text section (2 keys):

  • copied
  • notification-word-not-valid

ui section (11 keys):

  • about
  • avg_attempts
  • coming_soon
  • external_links
  • game
  • global_stats
  • losses
  • play
  • search_language
  • win_rate
  • wins

Add these keys with Spanish translations to match the default configuration requirements.

frontend/src/index-app.ts (2)

108-116: Missing try-catch around localStorage access.

The created() hook accesses localStorage without error handling. This can throw in private browsing mode or when storage is disabled.

🛠️ Suggested fix
         created() {
             // Load game results from localStorage
+            try {
                 const stored = localStorage.getItem('game_results');
                 if (stored) {
                     this.game_results = JSON.parse(stored);
                 } else {
                     this.game_results = {};
                     localStorage.setItem('game_results', JSON.stringify(this.game_results));
                 }
+            } catch {
+                // localStorage unavailable
+                this.game_results = {};
+            }

             // Cache languages for game page to access
             this.cacheLanguages();

Based on learnings, try-catch is required around localStorage access.


182-192: Missing try-catch around localStorage access in toggleDarkMode.

The toggleDarkMode() method accesses localStorage.setItem without error handling, unlike other toggle methods in this file (e.g., toggleHaptics, toggleSound) which properly wrap localStorage calls.

🛠️ Suggested fix
             toggleDarkMode(): void {
                 this.$nextTick(() => {
+                    try {
                         if (this.darkMode) {
                             document.documentElement.classList.add('dark');
                             localStorage.setItem('darkMode', 'true');
                         } else {
                             document.documentElement.classList.remove('dark');
                             localStorage.setItem('darkMode', 'false');
                         }
+                    } catch {
+                        // localStorage unavailable - dark mode still toggles visually
+                    }
                 });
             },

Based on learnings, try-catch is required around localStorage access.

tests/test_word_lists.py (1)

122-179: CI failing for sv/pau keyboard coverage.

The updated keyboard coverage now reports missing characters for sv and pau (uppercase letters/punctuation in word lists). Please reconcile word lists, keyboard layouts, or diacritic mappings so every word character is typeable; otherwise this test will keep failing in CI.

tests/test_language_config.py (1)

181-221: Align "↵" handling with test_word_lists.py for consistency.

test_word_lists.py (lines 150, 200) treats "↵" as a control key, but test_language_config.py doesn't. Add "↵" to both the control-key filter and enter-key check to maintain consistency across tests and prepare for potential future use.

Suggested adjustment
-                if key not in ("⇨", "⟹", "⌫", "ENTER", "DEL"):
+                if key not in ("⇨", "⟹", "↵", "⌫", "ENTER", "DEL"):
                     keyboard_chars.add(key)
@@
-        has_enter = "⇨" in all_keys or "⟹" in all_keys or "ENTER" in all_keys
+        has_enter = "⇨" in all_keys or "⟹" in all_keys or "↵" in all_keys or "ENTER" in all_keys
@@
-        assert has_enter, f"{lang}: Keyboard missing enter key (⇨, ⟹, or ENTER)"
+        assert has_enter, f"{lang}: Keyboard missing enter key (⇨, ⟹, ↵, or ENTER)"
tests/test_daily_word.py (1)

165-169: Fix data quality issue: pau word list contains 222 duplicate words.

The pau word list (webapp/data/languages/pau/pau_5words.txt) has 1947 total words but only 1725 unique words, meaning 222 words appear more than once. The test correctly fails because the algorithm cannot guarantee no repeated words within 365 days when the source list itself contains duplicates. Remove duplicate entries from the word list to resolve the test failure.

🤖 Fix all issues with AI agents
In `@frontend/src/game.ts`:
- Around line 506-536: The updateKeyColor function only applies diacritic
equivalence via normalizeMap and misses positional variants (final vs regular
forms), causing uncolored regular keys when a final-form char is typed; modify
updateKeyColor to also consult a positionalVariantsMap (or similar) to find and
update positional variants for both the typed char and its normalizedChar by
calling updateSingleKey for each variant, and likewise when iterating
normalizeMap ensure you also update any positional variants of the diacritic
keys and the base key; use the existing symbols updateKeyColor, updateSingleKey,
normalizeMap, keyClasses and char/normalizedChar to locate where to add the
extra lookups and updates.
- Around line 342-359: The countdown math assumes integer-hour timezone_offset
and miscomputes minutes/seconds for fractional offsets; replace the manual
localHours/utcMinutes/utcSeconds math by computing a single timestamp for "now"
in the target timezone (e.g., now.getTime() + timezone_offset * 3600_000) then
compute the next-local-midnight timestamp from that local timestamp and derive
remaining milliseconds, from which compute hours/minutes/seconds (h, m, s) using
integer division and modulo; update uses of now, timezone_offset, localHours, h,
m, s accordingly so half-hour/quarter-hour offsets are handled correctly.
- Around line 561-585: The win check currently normalizes only diacritics (using
normalizeWord with normalizeMap) but updateColors applies the fuller
normalization that also collapses positional variants; replace the current
normalizedGuess/normalizedTarget computation so both use the exact same
normalization logic as updateColors (i.e., call the same helper or normalization
function/parameters that updateColors uses) before comparing and calling
this.gameWon(); update the code around canonicalWord, normalizeWord,
normalizeMap, updateColors, and this.todays_word so they share the identical
normalization routine.

In `@tests/conftest.py`:
- Around line 100-103: Guard against empty or invalid multi-layouts by first
ensuring data["layouts"] is a non-empty mapping and that the chosen default key
exists; instead of using next(iter(...)) and indexing directly, check if
"layouts" has any keys, validate data.get("default") is in data["layouts"], and
if not, pick a safe fallback like the first key from data["layouts"] or raise a
clear error; update the block handling data (the variables data, default_layout
and the return of data["layouts"][default_layout]["rows"]) to perform these
checks and use the safe fallback before returning rows.

In `@tests/test_daily_word.py`:
- Around line 263-268: The current test test_legacy_algorithm_for_past_days only
asserts constants and doesn't exercise selection logic; replace the trivial
comparisons with assertions that call the code that decides which algorithm to
use (e.g., the function that computes the daily word or a helper like
uses_legacy_algorithm/get_algorithm_for_day) using day = MIGRATION_DAY_IDX and
day = MIGRATION_DAY_IDX + 1, and assert that the first call selects the legacy
algorithm and the second selects the new algorithm so the test verifies actual
behavior for MIGRATION_DAY_IDX boundary.

In `@webapp/app.py`:
- Around line 411-413: CI indicates formatting violations in webapp/app.py
around the default_language_config load (the open(...) line and surrounding
block). Reformat the file using the project's Black settings by running the
configured command (e.g., run black webapp/ tests/ as specified), or apply Black
in your editor/IDE, then stage the updated webapp/app.py so the
open(f"{data_dir}default_language_config.json", "r") -> default_language_config
assignment matches the repository's formatting.

In `@webapp/data/languages/bg/language_config.json`:
- Around line 35-62: The UI translations in the "ui" object of
language_config.json are incomplete and will fall back to English; compare this
"ui" block against the project's default language_config.json and add any
missing keys (e.g., all standard UI keys like menu labels, accessibility/toggle
descriptions, game-stats labels, install/update prompts) so every key present in
the default is present here, and also ensure required metadata fields (name,
native_name, direction/rtl if applicable) are present in the language config;
update the existing translations (e.g., "settings", "dark_mode", "install_app",
"close") to match the default key names exactly and provide Bulgarian
translations for any keys you add.

In `@webapp/data/languages/ca/ca_keyboard.json`:
- Around line 12-18: The "catalan_extended" keyboard layout currently omits the
Catalan middle dot needed for "l·l"; update the "catalan_extended" object so the
rows array includes the "·" character (e.g., add "·" next to "ŀ" or "l" in the
second row) ensuring the extended layout exposes the middle dot key for Catalan
input; modify the rows value inside the "catalan_extended" entry to include "·"
while preserving existing keys like "ŀ" and "ç".

In `@webapp/data/languages/is/is_keyboard.json`:
- Around line 1-21: The Icelandic keyboard layout ("icelandic_qwerty" in
is_keyboard.json) includes four characters not present in the Icelandic word
lists (c, q, w, z); remove these keys from the "rows" arrays under the
"icelandic_qwerty" layout if they are unintended, or alternatively add a short
comment/entry in the file documenting why "c", "q", "w", and "z" are retained
(e.g., for loanwords or user familiarity) so the discrepancy is explicit; ensure
any change preserves row structure and the special keys "⇨" and "⌫" remain
unchanged.

In `@webapp/data/languages/it/it_keyboard.json`:
- Around line 1-21: The default "italian_qwerty" layout lacks Italian accented
vowels used by the word lists; update the JSON so users can input à, è, é, ì, ò,
ó, ù by either setting "default" to "italian_extended" or by merging the
accented vowels into the "italian_qwerty" "rows" arrays (e.g., add accented
characters to appropriate row positions or expand keys) and ensure the "layouts"
keys "italian_qwerty" and "italian_extended" remain valid with the chosen
default.

In `@webapp/data/languages/la/language_config.json`:
- Around line 35-63: The "ui" JSON entry for the key "close" currently has the
value "claude"—verify that this is the intended Latin translation for the UI
button; if it's incorrect replace the value with the agreed-upon Latin
imperative or short UI label (e.g., the correct form of "to close") and ensure
the "close" value matches the style/casing of other UI labels in the "ui"
object; update the "close" property in the same "ui" block and run the
localization/JSON lint checks after change.

In `@webapp/data/languages/lt/lt_keyboard.json`:
- Around line 7-9: The default Lithuanian keyboard layout in lt_keyboard.json
contains unsupported Latin letters (e.g., "q", "w" in the first row array and
"x" in the third row array) that should be removed or moved to an
optional/extended layout; update the three top-level key arrays (the first-row
array starting with ["q","w","e","ė"...], the second-row array starting with
["a","ą","s"...], and the third-row array starting with ["⇨","z","ž","x"...]) so
that unsupported letters are excluded from the default layout and, if needed,
placed into a separate extended/alternate layout definition used only when
allowed.

In `@webapp/data/languages/lv/language_config.json`:
- Around line 35-62: The UI translations block is missing 19 keys, causing
fallbacks; add the listed keys into the existing "ui" object in
language_config.json with appropriate Latvian strings for: about, avg_attempts,
best_active_streak, best_overall_streak, best_streak, coming_soon,
current_streak, external_links, game, games_lowercase, games_played,
global_stats, languages_won, losses, no_games_yet, play, search_language,
win_rate, wins, and your_languages; ensure each key name exactly matches the
baseline keys and provide correct Latvian translations (matching the style of
nearby entries like "settings" and "games") so the UI stops falling back to
English.

In `@webapp/data/languages/nb/language_config.json`:
- Around line 5-8: The diacritic_map is incomplete: update the "diacritic_map"
object to include mappings for the missing characters found in word lists (à, å,
æ, ø) or document that they are native letters; specifically add entries mapping
'a' -> ['à'] (or include 'à' in the existing 'a' mapping), 'a' or 'aa' -> ['å']
depending on intended normalization, 'ae' or 'a' -> ['æ'] and 'o' -> ['ø'] (or
preserve them as native letters if that is the intended behavior) so all
non-ASCII characters in the lists are handled; modify the diacritic_map key
exactly named "diacritic_map" and ensure any normalization logic that consumes
it uses these new entries consistently.

In `@webapp/data/languages/nb/nb_keyboard.json`:
- Around line 15-17: The alphabetical keyboard row in nb_keyboard.json is
missing the "q" key; update the second row array (currently containing
"l","m","n","o","p","r","s","t","u","v","w") to insert "q" between "p" and "r"
and, to keep row widths uniform, move "w" into the third row array (the one with
"⇨","x","y","z","æ","ø","å","⌫"), ensuring the rows still include the full
Norwegian characters "æ","ø","å".

In `@webapp/data/languages/nn/language_config.json`:
- Around line 5-8: The Nynorsk JSON is missing the required "ui" translations
block, so add a top-level "ui" object alongside the existing "diacritic_map"
containing the 26 keys used by the app (settings, dark_mode, haptic_feedback,
sound_effects, keyboard_layout, easy_mode, install_app, report_issue,
view_source, guess_distribution, games, win_percent, streak, best,
all_languages, languages, play_more_languages, add_to_home, play_daily_like_app,
install, close, dark_mode_desc, haptic_feedback_desc, sound_effects_desc,
easy_mode_label, right_to_left) and provide the Nynorsk translations for each
key (follow the pattern used in other language JSONs in this PR); ensure the
"ui" key is a sibling of "diacritic_map" so the app can read these labels.

In `@webapp/data/languages/nn/nn_keyboard.json`:
- Around line 12-19: The "alphabetical" keyboard layout is missing the letter
'q' in its second row; update the "alphabetical" object's "rows" array so that
the second subarray (the one currently
["l","m","n","o","p","r","s","t","u","v","w"]) includes "q" between "p" and "r"
(i.e., ["l","m","n","o","p","q","r","s","t","u","v","w"]) so words with 'q' can
be typed.

In `@webapp/data/languages/pt/language_config.json`:
- Around line 43-70: The ui translation block (the "ui" object containing keys
like "settings", "dark_mode", "install_app", "close") is missing several
required keys from the default language_config.json causing English fallbacks;
update the "ui" object to include all keys present in the default config (e.g.,
any missing labels such as share/feedback/install prompts/labels, help/tooltips,
guess distribution captions, etc.) and ensure the file also contains the
required language metadata fields (language name, native_name, locale, and text
direction like "right_to_left") to match the canonical config; locate the "ui"
object and add the missing keys with Portuguese translations consistent with
existing keys so no defaults are used.

In `@webapp/data/languages/ru/language_config.json`:
- Around line 35-62: The ru language JSON's "ui" block is missing several
required keys from the default config, causing English fallbacks; open the
default language_config.json and add every UI key present there into the "ui"
object in this file (translate values into Russian or add a temporary
Russian-visible placeholder), and also ensure top-level metadata keys (e.g.,
name, nativeName, dir, and any language-specific metadata) are present and
correctly set; preserve the same key names (e.g., keys under "ui" like any
missing labels, tooltips, share texts, error messages, etc.), keep JSON valid
(commas/quoting), and run a quick linter/test to confirm no fallbacks remain.

In `@webapp/data/languages/vi/language_config.json`:
- Around line 35-62: The vi language_config.json "ui" block is missing 20 UI
keys causing English fallbacks; add the missing keys into the "ui" object (e.g.,
about, avg_attempts, best_active_streak, best_overall_streak, best_streak,
coming_soon, current_streak, external_links, game, games_lowercase,
games_played, global_stats, languages_won, losses, no_games_yet, play,
search_language, win_rate, wins, your_languages) with appropriate Vietnamese
translations, keeping key names exactly as listed so consumers
(language_config.json -> "ui") pick them up; ensure translations match the style
of existing entries (short labels and optional *_desc where applicable) and
validate the JSON after inserting the new fields.
♻️ Duplicate comments (2)
webapp/templates/game.html (1)

116-125: Guard against missing language.key_diacritic_hints.

If a language doesn't define key_diacritic_hints, calling .get() on None/undefined will throw an error and prevent the page from rendering.

🛡️ Safe default
-{% set hint = language.key_diacritic_hints.get(c.lower(), {}) %}
+{% set diacritic_hints = language.key_diacritic_hints | default({}) %}
+{% set hint = diacritic_hints.get(c.lower(), {}) %}
 {% set hint_text = hint.text if hint else '' %}
 {% set hint_above = hint.above if hint else false %}
webapp/app.py (1)

229-250: Handle malformed language configs with logging.

FileNotFoundError is covered, but a malformed or unreadable JSON will currently 500. Consider catching json.JSONDecodeError/PermissionError and logging before falling back to defaults.

✅ Proposed fix
-    except FileNotFoundError:
-        return default_config
+    except FileNotFoundError:
+        return default_config
+    except json.JSONDecodeError as exc:
+        app.logger.warning("Invalid language_config.json for %s: %s", lang, exc)
+        return default_config
+    except PermissionError as exc:
+        app.logger.warning("Cannot read language_config.json for %s: %s", lang, exc)
+        return default_config
🧹 Nitpick comments (12)
webapp/data/languages/en/language_config.json (1)

35-62: Normalize UI label casing for consistency.
Most labels here are Title Case, but "close" is lowercase. Consider “Close” to align with the rest of the UI labels and help.close.

💡 Proposed tweak
-        "close": "close"
+        "close": "Close"
webapp/data/languages/fo/fo_keyboard.json (1)

7-9: Consider whether non‑Faroese letters belong in the default layout.

The QWERTY rows include q, w, c, x, z, which are not part of the Faroese alphabet. If your Faroese word lists don’t use them, they become dead keys and expand the guess space unnecessarily. Consider removing them or moving them to an “extended” layout. As per coding guidelines, ensure keyboard layouts match the language character set.

frontend/src/style.css (1)

37-40: Redundant CSS rule — .hint-above already inherits flex-col from .has-hint.

The .key.has-hint.hint-above rule only applies flex-col, which is already declared on .key.has-hint (line 32). This class currently has no effect.

If the intent is to support a .hint-below variant in the future, consider removing this rule until needed, or add a differentiating style (e.g., flex-col-reverse for below).

-/* When hint is above the main letter (for keys with many variants) */
-.key.has-hint.hint-above {
-    `@apply` flex-col;
-}
playwright.config.ts (1)

33-39: Unix-specific webServer command may fail on Windows.

The shell command uses POSIX syntax (. venv/bin/activate) which won't work on Windows. If Windows development is supported, consider adding platform detection or documenting this limitation.

💡 Cross-platform alternative (if needed)
     webServer: {
-        command:
-            ". venv/bin/activate && gunicorn --chdir webapp --bind 127.0.0.1:8000 app:app",
+        command:
+            process.platform === "win32"
+                ? "venv\\Scripts\\activate && gunicorn --chdir webapp --bind 127.0.0.1:8000 app:app"
+                : ". venv/bin/activate && gunicorn --chdir webapp --bind 127.0.0.1:8000 app:app",
         url: "http://127.0.0.1:8000",
         reuseExistingServer: !process.env.CI,
         timeout: 30000,
     },
frontend/src/positional.ts (2)

67-83: Consider caching or accepting the reverseMap as a parameter.

normalizePositional rebuilds the reverse map on every invocation. If this function is called frequently (e.g., per word in a list), this creates unnecessary overhead.

♻️ Optional: Accept pre-built reverseMap
-export function normalizePositional(word: string, config: PositionalConfig): string {
-    if (!config.final_form_map || word.length === 0) return word;
-
-    const chars = [...word];
-    const lastIdx = chars.length - 1;
-    const reverseMap = buildFinalFormReverseMap(config);
+export function normalizePositional(
+    word: string,
+    config: PositionalConfig,
+    reverseMap?: Map<string, string>
+): string {
+    if (!config.final_form_map || word.length === 0) return word;
+
+    const chars = [...word];
+    const lastIdx = chars.length - 1;
+    const map = reverseMap ?? buildFinalFormReverseMap(config);

92-95: normalizePositionalChar duplicates toRegularForm.

Both functions have identical logic. Consider reusing toRegularForm to reduce duplication.

♻️ Reuse toRegularForm
 export function normalizePositionalChar(char: string, reverseMap: Map<string, string>): string {
-    // Final forms normalize to their regular form
-    return reverseMap.get(char) || char;
+    return toRegularForm(char, reverseMap);
 }
tests/test_daily_word.py (1)

277-285: Annotate mutable class attribute with ClassVar.

Per static analysis (RUF012), mutable class attributes should use typing.ClassVar to clarify intent and avoid accidental mutation across instances.

♻️ Add ClassVar annotation
+from typing import ClassVar
+
 class TestBackwardsCompatibility:
     """Tests to ensure past daily words don't change."""

     # Known daily words for specific dates (captured before migration)
     # Format: (lang, date, expected_word)
     # These are the ACTUAL words that were shown on these dates
-    KNOWN_WORDS = [
+    KNOWN_WORDS: ClassVar[list] = [
         # English - well-known, stable
         ("en", datetime.date(2025, 1, 1), None),  # Will be filled by test
scripts/curate_words.py (4)

32-43: Use explicit None union in type hint.

PEP 484 prohibits implicit Optional. Use T | None syntax for clarity.

♻️ Fix type hint
-def get_todays_idx(date: datetime.date = None) -> int:
+def get_todays_idx(date: datetime.date | None = None) -> int:

82-82: Remove extraneous f-string prefix.

This string has no placeholders, so the f prefix is unnecessary.

♻️ Remove f prefix
-    print(f"Review the file and identify words to remove.")
+    print("Review the file and identify words to remove.")

93-97: list.remove() in a loop is O(n²) for large removals.

Each remove() call scans the entire list. For bulk removal from large word lists, consider using a set difference.

♻️ More efficient removal
 def remove_words(lang: str, words_to_remove: list[str]) -> None:
     """Remove specific words from the word list."""
     words = load_word_list(lang)
     original_count = len(words)

-    # Find and remove words
-    removed = []
-    not_found = []
-    for word in words_to_remove:
-        word = word.strip().lower()
-        if word in words:
-            words.remove(word)
-            removed.append(word)
-        else:
-            not_found.append(word)
+    # Normalize words to remove
+    to_remove_set = {w.strip().lower() for w in words_to_remove}
+    words_set = set(words)
+    
+    removed = [w for w in to_remove_set if w in words_set]
+    not_found = [w for w in to_remove_set if w not in words_set]
+    
+    # Filter while preserving order
+    words = [w for w in words if w not in to_remove_set]

32-43: Consider extracting shared utilities to reduce code duplication.

get_todays_idx and load_blocklist are duplicated across webapp/app.py, tests/test_daily_word.py, and this script. Consider creating a shared utility module to maintain consistency and reduce maintenance burden.

Also applies to: 148-159

e2e/smoke.spec.ts (1)

126-141: Consider using test.describe.each for parameterized language tests.

Using a for loop works but test.describe.each provides better Playwright integration, including parallel execution and clearer test naming.

♻️ Using test.describe.each pattern
const languages = ["en", "es", "fr", "de", "it", "ru", "he", "ar"];

for (const lang of languages) {
    test(`${lang} game page loads`, async ({ page }) => {
        // ... existing test code
    });
}

Note: The current implementation is functionally correct. This is just a stylistic suggestion for better Playwright idioms.

Comment thread frontend/src/game.ts
Comment on lines +342 to +359
// Calculate time until midnight in the language's timezone
const now = new Date();

// Get current UTC time and apply the language's timezone offset
const utcHours = now.getUTCHours();
const utcMinutes = now.getUTCMinutes();
const utcSeconds = now.getUTCSeconds();

// Calculate hours in the language's local timezone
let localHours = utcHours + timezone_offset;
// Handle day wraparound
if (localHours >= 24) localHours -= 24;
if (localHours < 0) localHours += 24;

// Time until midnight in the language's timezone
const h = 23 - localHours;
const m = 59 - utcMinutes;
const s = 59 - utcSeconds;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Handle fractional timezone offsets in countdown.

The current math assumes whole-hour offsets; half-hour/quarter-hour timezones will show incorrect time remaining. Consider computing using Date arithmetic so fractional offsets are handled correctly.

✅ Proposed fix
-                const now = new Date();
-
-                // Get current UTC time and apply the language's timezone offset
-                const utcHours = now.getUTCHours();
-                const utcMinutes = now.getUTCMinutes();
-                const utcSeconds = now.getUTCSeconds();
-
-                // Calculate hours in the language's local timezone
-                let localHours = utcHours + timezone_offset;
-                // Handle day wraparound
-                if (localHours >= 24) localHours -= 24;
-                if (localHours < 0) localHours += 24;
-
-                // Time until midnight in the language's timezone
-                const h = 23 - localHours;
-                const m = 59 - utcMinutes;
-                const s = 59 - utcSeconds;
+                const now = new Date();
+                const localNow = new Date(now.getTime() + timezone_offset * 3600 * 1000);
+                const nextMidnight = new Date(localNow);
+                nextMidnight.setHours(24, 0, 0, 0);
+
+                const diffMs = nextMidnight.getTime() - localNow.getTime();
+                const h = Math.floor(diffMs / 3_600_000);
+                const m = Math.floor((diffMs % 3_600_000) / 60_000);
+                const s = Math.floor((diffMs % 60_000) / 1000);
🤖 Prompt for AI Agents
In `@frontend/src/game.ts` around lines 342 - 359, The countdown math assumes
integer-hour timezone_offset and miscomputes minutes/seconds for fractional
offsets; replace the manual localHours/utcMinutes/utcSeconds math by computing a
single timestamp for "now" in the target timezone (e.g., now.getTime() +
timezone_offset * 3600_000) then compute the next-local-midnight timestamp from
that local timestamp and derive remaining milliseconds, from which compute
hours/minutes/seconds (h, m, s) using integer division and modulo; update uses
of now, timezone_offset, localHours, h, m, s accordingly so
half-hour/quarter-hour offsets are handled correctly.

Comment thread frontend/src/game.ts
Comment on lines +506 to 536
updateKeyColor(
char: string,
newState: KeyState,
keyClasses: Record<string, KeyState>
): void {
const updateSingleKey = (key: string, state: KeyState) => {
const current = keyClasses[key];
// Priority: key-correct > key-semicorrect > key-incorrect
if (current === 'key-correct') return;
if (current === 'key-semicorrect' && state === 'key-incorrect') return;
keyClasses[key] = state;
};

// Update the typed character
updateSingleKey(char, newState);

// Also update equivalent diacritical characters
const normalizedChar = normalizeMap.get(char) || char;

// Find all chars that normalize to the same base and update them too
for (const [diacritic, base] of normalizeMap.entries()) {
if (base === normalizedChar) {
updateSingleKey(diacritic, newState);
}
}

// Also update the base char if we typed a diacritic
if (normalizeMap.has(char)) {
updateSingleKey(normalizedChar, newState);
}
},
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Key color updates ignore positional variants.

When a final-form character is typed (e.g., ך/ς), only diacritic equivalence is updated. This can leave the regular-form key uncolored on keyboards that don’t include final forms.

✅ Proposed fix
-                // Update the typed character
-                updateSingleKey(char, newState);
-
-                // Also update equivalent diacritical characters
-                const normalizedChar = normalizeMap.get(char) || char;
+                // Update typed + positional-normalized variants
+                updateSingleKey(char, newState);
+                const regularChar = toRegularForm(char, finalFormReverseMap);
+                updateSingleKey(regularChar, newState);
+                const finalChar = positionalConfig.final_form_map?.[regularChar];
+                if (finalChar) updateSingleKey(finalChar, newState);
+
+                // Also update equivalent diacritical characters
+                const normalizedChar = normalizeMap.get(regularChar) || regularChar;
🤖 Prompt for AI Agents
In `@frontend/src/game.ts` around lines 506 - 536, The updateKeyColor function
only applies diacritic equivalence via normalizeMap and misses positional
variants (final vs regular forms), causing uncolored regular keys when a
final-form char is typed; modify updateKeyColor to also consult a
positionalVariantsMap (or similar) to find and update positional variants for
both the typed char and its normalizedChar by calling updateSingleKey for each
variant, and likewise when iterating normalizeMap ensure you also update any
positional variants of the diacritic keys and the base key; use the existing
symbols updateKeyColor, updateSingleKey, normalizeMap, keyClasses and
char/normalizedChar to locate where to add the extra lookups and updates.

Comment thread frontend/src/game.ts
Comment on lines 561 to 585
const row = this.tiles[this.active_row];
const word = row ? row.join('').toLowerCase() : '';
if (this.checkWord(word)) {
const typedWord = row ? row.join('').toLowerCase() : '';
const canonicalWord = this.checkWord(typedWord);

if (canonicalWord) {
haptic.confirm(); // Valid word submitted

// Update tiles to show canonical form (with diacritics)
// This displays the correct accented letters after submission
if (row && canonicalWord !== typedWord) {
for (let i = 0; i < canonicalWord.length; i++) {
row.splice(i, 1, canonicalWord[i]);
}
}

this.updateColors();
this.active_row++;
this.active_cell = 0;
this.full_word_inputted = false;

if (word === this.todays_word) {
// Compare normalized forms for win detection
const normalizedGuess = normalizeWord(canonicalWord, normalizeMap);
const normalizedTarget = normalizeWord(this.todays_word, normalizeMap);
if (normalizedGuess === normalizedTarget) {
this.gameWon();
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Win detection should use the same full normalization as coloring.

updateColors treats positional variants as equivalent, but win detection only normalizes diacritics. This can produce an all-green board while still failing the win check for positional scripts.

✅ Proposed fix
-                        const normalizedGuess = normalizeWord(canonicalWord, normalizeMap);
-                        const normalizedTarget = normalizeWord(this.todays_word, normalizeMap);
+                        const normalizeForWin = (w: string): string =>
+                            [...w]
+                                .map((c) => {
+                                    const regular = toRegularForm(c, finalFormReverseMap);
+                                    return normalizeMap.get(regular) || regular;
+                                })
+                                .join('');
+                        const normalizedGuess = normalizeForWin(canonicalWord);
+                        const normalizedTarget = normalizeForWin(this.todays_word);
🤖 Prompt for AI Agents
In `@frontend/src/game.ts` around lines 561 - 585, The win check currently
normalizes only diacritics (using normalizeWord with normalizeMap) but
updateColors applies the fuller normalization that also collapses positional
variants; replace the current normalizedGuess/normalizedTarget computation so
both use the exact same normalization logic as updateColors (i.e., call the same
helper or normalization function/parameters that updateColors uses) before
comparing and calling this.gameWon(); update the code around canonicalWord,
normalizeWord, normalizeMap, updateColors, and this.todays_word so they share
the identical normalization routine.

Comment thread tests/conftest.py
Comment on lines +100 to +103
# New multi-layout format
if isinstance(data, dict) and "layouts" in data:
default_layout = data.get("default", next(iter(data["layouts"])))
return data["layouts"][default_layout]["rows"]
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Guard against empty/invalid layouts in multi‑layout keyboards.

If layouts is empty or default is missing/invalid, this will raise StopIteration / KeyError and fail tests. Consider a safe fallback similar to the app loader.

🔧 Proposed fix (safe fallback)
-    if isinstance(data, dict) and "layouts" in data:
-        default_layout = data.get("default", next(iter(data["layouts"])))
-        return data["layouts"][default_layout]["rows"]
+    if isinstance(data, dict) and isinstance(data.get("layouts"), dict):
+        layouts = data["layouts"]
+        if not layouts:
+            return None
+        default_layout = data.get("default")
+        if default_layout not in layouts:
+            default_layout = next(iter(layouts), None)
+        if default_layout is None:
+            return None
+        return layouts[default_layout].get("rows", [])
🤖 Prompt for AI Agents
In `@tests/conftest.py` around lines 100 - 103, Guard against empty or invalid
multi-layouts by first ensuring data["layouts"] is a non-empty mapping and that
the chosen default key exists; instead of using next(iter(...)) and indexing
directly, check if "layouts" has any keys, validate data.get("default") is in
data["layouts"], and if not, pick a safe fallback like the first key from
data["layouts"] or raise a clear error; update the block handling data (the
variables data, default_layout and the return of
data["layouts"][default_layout]["rows"]) to perform these checks and use the
safe fallback before returning rows.

Comment thread tests/test_daily_word.py
Comment on lines +263 to +268
def test_legacy_algorithm_for_past_days(self):
"""Days <= MIGRATION_DAY_IDX should use legacy algorithm."""
# Day 1681 should still use legacy
assert MIGRATION_DAY_IDX <= 1681
# Day 1682 should use new algorithm
assert MIGRATION_DAY_IDX < 1682
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Assertions are trivially true and don't test meaningful behavior.

These assertions (MIGRATION_DAY_IDX <= 1681 and MIGRATION_DAY_IDX < 1682) are always true since MIGRATION_DAY_IDX = 1681. Consider testing actual algorithm selection behavior instead.

💡 Suggested improvement
     def test_legacy_algorithm_for_past_days(self):
         """Days <= MIGRATION_DAY_IDX should use legacy algorithm."""
-        # Day 1681 should still use legacy
-        assert MIGRATION_DAY_IDX <= 1681
-        # Day 1682 should use new algorithm
-        assert MIGRATION_DAY_IDX < 1682
+        # Verify the cutoff constant has the expected value
+        assert MIGRATION_DAY_IDX == 1681, "Migration cutoff should be day 1681"
+        # Optionally: test that algorithm selection differs at boundary
🤖 Prompt for AI Agents
In `@tests/test_daily_word.py` around lines 263 - 268, The current test
test_legacy_algorithm_for_past_days only asserts constants and doesn't exercise
selection logic; replace the trivial comparisons with assertions that call the
code that decides which algorithm to use (e.g., the function that computes the
daily word or a helper like uses_legacy_algorithm/get_algorithm_for_day) using
day = MIGRATION_DAY_IDX and day = MIGRATION_DAY_IDX + 1, and assert that the
first call selects the legacy algorithm and the second selects the new algorithm
so the test verifies actual behavior for MIGRATION_DAY_IDX boundary.

Comment on lines +5 to +8
"diacritic_map": {
"e": ["é", "è"],
"o": ["ó", "ò", "ô"]
},
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# First, let's see what language config files exist
find webapp/data/languages -name "language_config.json" -type f 2>/dev/null | head -20

Repository: Hugo0/wordle

Length of output: 977


🏁 Script executed:

#!/bin/bash
# Run the provided script to check for missing UI keys in Nynorsk
python - <<'PY'
import json, pathlib

base = None
for p in pathlib.Path("webapp/data/languages").rglob("language_config.json"):
    data = json.loads(p.read_text(encoding="utf-8"))
    if data.get("language_code") == "en":
        base = data
        break

if base is None:
    print("No base language_config with language_code == 'en' found.")
    raise SystemExit(1)

nn = json.loads(pathlib.Path("webapp/data/languages/nn/language_config.json").read_text(encoding="utf-8"))

base_ui = set(base.get("ui", {}).keys())
nn_ui = set(nn.get("ui", {}).keys())

print("Base (en) UI keys:", sorted(base_ui))
print("Nynorsk (nn) UI keys:", sorted(nn_ui))
print("Missing ui keys in nn:", sorted(base_ui - nn_ui))
PY

Repository: Hugo0/wordle

Length of output: 992


🏁 Script executed:

#!/bin/bash
# Let's examine the actual Nynorsk config to see its structure
cat -n webapp/data/languages/nn/language_config.json

Repository: Hugo0/wordle

Length of output: 1902


🏁 Script executed:

#!/bin/bash
# Let's also check the base English config structure
cat -n webapp/data/languages/en/language_config.json

Repository: Hugo0/wordle

Length of output: 3126


🏁 Script executed:

#!/bin/bash
# Search for any fallback logic in the webapp for missing UI translations
rg -i "fallback|missing|ui.*key|gettext|i18n|translate" webapp --type js --type ts --type json -A 2 | head -100

Repository: Hugo0/wordle

Length of output: 409


Add the missing ui block with required UI translations for Nynorsk.

The Nynorsk config is missing the entire ui block that contains 26 required UI labels (settings, dark_mode, haptic_feedback, sound_effects, keyboard_layout, easy_mode, install_app, report_issue, view_source, guess_distribution, games, win_percent, streak, best, all_languages, languages, play_more_languages, add_to_home, play_daily_like_app, install, close, dark_mode_desc, haptic_feedback_desc, sound_effects_desc, easy_mode_label, right_to_left). English and other languages in this PR include this block. Without it, Nynorsk will display undefined or missing labels in the UI. Add the ui translations to comply with the coding guideline.

🤖 Prompt for AI Agents
In `@webapp/data/languages/nn/language_config.json` around lines 5 - 8, The
Nynorsk JSON is missing the required "ui" translations block, so add a top-level
"ui" object alongside the existing "diacritic_map" containing the 26 keys used
by the app (settings, dark_mode, haptic_feedback, sound_effects,
keyboard_layout, easy_mode, install_app, report_issue, view_source,
guess_distribution, games, win_percent, streak, best, all_languages, languages,
play_more_languages, add_to_home, play_daily_like_app, install, close,
dark_mode_desc, haptic_feedback_desc, sound_effects_desc, easy_mode_label,
right_to_left) and provide the Nynorsk translations for each key (follow the
pattern used in other language JSONs in this PR); ensure the "ui" key is a
sibling of "diacritic_map" so the app can read these labels.

Comment on lines +12 to +19
"alphabetical": {
"label": "Alphabetical",
"rows": [
["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k"],
["l", "m", "n", "o", "p", "r", "s", "t", "u", "v", "w"],
["⇨", "x", "y", "z", "æ", "ø", "å", "⌫"]
]
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# First, check if the nn_keyboard.json file exists and is valid JSON
if [ -f "webapp/data/languages/nn/nn_keyboard.json" ]; then
    echo "=== Checking JSON validity ==="
    python3 << 'EOFPYTHON'
import json
try:
    with open("webapp/data/languages/nn/nn_keyboard.json", 'r', encoding='utf-8') as f:
        data = json.load(f)
    print("✓ JSON is valid")
    print("\nKeyboard layouts available:", list(data.get('layouts', {}).keys()))
    
    # Check alphabetical layout
    alphabetical = data.get('layouts', {}).get('alphabetical', {})
    if alphabetical:
        rows = alphabetical.get('rows', [])
        print("\nAlphabetical layout rows:")
        for i, row in enumerate(rows):
            print(f"  Row {i}: {row}")
        
        # Check for 'q'
        all_chars = ''.join([''.join(row) if isinstance(row, list) else row for row in rows])
        if 'q' in all_chars.lower():
            print("\n✓ 'q' found in alphabetical layout")
        else:
            print("\n✗ 'q' NOT found in alphabetical layout")
except Exception as e:
    print(f"✗ Error: {e}")
EOFPYTHON
else
    echo "File not found"
fi

Repository: Hugo0/wordle

Length of output: 142


🏁 Script executed:

#!/bin/bash
# Check for Norwegian word lists and search for words with 'q'
echo "=== Searching for Norwegian word files ==="
fd -e txt . webapp/data/languages/nn/ 2>/dev/null || echo "No nn directory found"

echo -e "\n=== Checking for 'q' in Norwegian words ==="
# Check common word list files
for file in webapp/data/languages/nn/nn_*.txt; do
    if [ -f "$file" ]; then
        count=$(grep -ic 'q' "$file" || echo "0")
        if [ "$count" -gt 0 ]; then
            echo "File: $file - Words with 'q': $count"
            grep -i 'q' "$file" | head -5
        fi
    fi
done

Repository: Hugo0/wordle

Length of output: 391


🏁 Script executed:

#!/bin/bash
# Compare with other keyboard layouts to see if 'q' is consistently included/excluded
echo "=== Checking other keyboard layouts ==="
fd nn_keyboard.json . 2>/dev/null && echo "Found nn_keyboard.json"

# Check a few other language keyboards for comparison
echo -e "\n=== Checking alphabetical layouts in other languages ==="
for keyboard in webapp/data/languages/*/alphabetical_keyboard.json; do
    if [ -f "$keyboard" ]; then
        lang=$(echo "$keyboard" | cut -d'/' -f4)
        has_q=$(grep -c '"q"' "$keyboard" 2>/dev/null || echo "0")
        if [ "$has_q" -gt 0 ]; then
            echo "$lang: has 'q' in alphabetical"
        else
            echo "$lang: NO 'q' in alphabetical"
        fi
    fi
done

Repository: Hugo0/wordle

Length of output: 220


🏁 Script executed:

#!/bin/bash
# Read the nn_keyboard.json file to confirm the exact state
cat webapp/data/languages/nn/nn_keyboard.json

Repository: Hugo0/wordle

Length of output: 755


Add missing 'q' to alphabetical layout

The alphabetical layout is missing the letter 'q' (row 2 goes from "p" directly to "r"). This is a bug since the word list contains 5 words with 'q' (qatar, quito, quart, squaw, quipu) that cannot be typed using the alphabetical layout. Insert "q" between "p" and "r" in row 2.

🤖 Prompt for AI Agents
In `@webapp/data/languages/nn/nn_keyboard.json` around lines 12 - 19, The
"alphabetical" keyboard layout is missing the letter 'q' in its second row;
update the "alphabetical" object's "rows" array so that the second subarray (the
one currently ["l","m","n","o","p","r","s","t","u","v","w"]) includes "q"
between "p" and "r" (i.e., ["l","m","n","o","p","q","r","s","t","u","v","w"]) so
words with 'q' can be typed.

Comment on lines +43 to +70
"ui": {
"settings": "Definições",
"dark_mode": "Modo escuro",
"dark_mode_desc": "Alternar tema escuro",
"haptic_feedback": "Resposta háptica",
"haptic_feedback_desc": "Vibração ao tocar",
"sound_effects": "Efeitos sonoros",
"sound_effects_desc": "Sons de vitória/derrota",
"right_to_left": "Direita para esquerda",
"keyboard_layout": "Layout do teclado",
"easy_mode": "Permitir qualquer palavra",
"easy_mode_label": "modo fácil",
"install_app": "Instalar aplicação",
"install_app_desc": "Jogue offline e obtenha o ícone",
"report_issue": "Reportar um problema",
"view_source": "Ver código fonte",
"guess_distribution": "Distribuição de tentativas",
"games": "Jogos",
"win_percent": "% Vitórias",
"streak": "Sequência",
"best": "Melhor",
"all_languages": "Todos os idiomas",
"languages": "Idiomas",
"play_more_languages": "Jogue mais idiomas para ver suas estatísticas globais!",
"add_to_home": "Adicionar ao ecrã inicial",
"play_daily_like_app": "Jogue Wordle diariamente como uma app",
"install": "Instalar",
"close": "fechar"
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Ensure UI translations cover required keys.

The ui block appears to omit several keys that exist in the default config, which can lead to English fallbacks. As per coding guidelines, ensure language_config.json contains required UI translations and metadata for each language.

🤖 Prompt for AI Agents
In `@webapp/data/languages/pt/language_config.json` around lines 43 - 70, The ui
translation block (the "ui" object containing keys like "settings", "dark_mode",
"install_app", "close") is missing several required keys from the default
language_config.json causing English fallbacks; update the "ui" object to
include all keys present in the default config (e.g., any missing labels such as
share/feedback/install prompts/labels, help/tooltips, guess distribution
captions, etc.) and ensure the file also contains the required language metadata
fields (language name, native_name, locale, and text direction like
"right_to_left") to match the canonical config; locate the "ui" object and add
the missing keys with Portuguese translations consistent with existing keys so
no defaults are used.

Comment on lines +35 to +62
"ui": {
"settings": "Настройки",
"dark_mode": "Тёмный режим",
"dark_mode_desc": "Переключить тёмную тему",
"haptic_feedback": "Тактильная отдача",
"haptic_feedback_desc": "Вибрация при касании",
"sound_effects": "Звуковые эффекты",
"sound_effects_desc": "Звуки победы/поражения",
"right_to_left": "Справа налево",
"keyboard_layout": "Раскладка клавиатуры",
"easy_mode": "Разрешить любое слово",
"easy_mode_label": "лёгкий режим",
"install_app": "Установить приложение",
"install_app_desc": "Играйте офлайн и получите иконку",
"report_issue": "Сообщить о проблеме",
"view_source": "Посмотреть исходный код",
"guess_distribution": "Распределение попыток",
"games": "Игры",
"win_percent": "Побед %",
"streak": "Серия",
"best": "Лучший",
"all_languages": "Все языки",
"languages": "Языки",
"play_more_languages": "Играйте на других языках для общей статистики!",
"add_to_home": "Добавить на главный экран",
"play_daily_like_app": "Играйте в Wordle ежедневно как в приложение",
"install": "Установить",
"close": "закрыть"
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Ensure UI translations cover required keys.

The ui block appears to omit several keys that exist in the default config, which can lead to English fallbacks. As per coding guidelines, ensure language_config.json contains required UI translations and metadata for each language.

🤖 Prompt for AI Agents
In `@webapp/data/languages/ru/language_config.json` around lines 35 - 62, The ru
language JSON's "ui" block is missing several required keys from the default
config, causing English fallbacks; open the default language_config.json and add
every UI key present there into the "ui" object in this file (translate values
into Russian or add a temporary Russian-visible placeholder), and also ensure
top-level metadata keys (e.g., name, nativeName, dir, and any language-specific
metadata) are present and correctly set; preserve the same key names (e.g., keys
under "ui" like any missing labels, tooltips, share texts, error messages,
etc.), keep JSON valid (commas/quoting), and run a quick linter/test to confirm
no fallbacks remain.

Comment on lines +35 to +62
"ui": {
"settings": "Cài đặt",
"dark_mode": "Chế độ tối",
"dark_mode_desc": "Chuyển đổi giao diện tối",
"haptic_feedback": "Phản hồi xúc giác",
"haptic_feedback_desc": "Rung khi chạm",
"sound_effects": "Hiệu ứng âm thanh",
"sound_effects_desc": "Âm thanh thắng/thua",
"right_to_left": "Phải sang trái",
"keyboard_layout": "Bố cục bàn phím",
"easy_mode": "Cho phép mọi từ",
"easy_mode_label": "chế độ dễ",
"install_app": "Cài đặt ứng dụng",
"install_app_desc": "Chơi ngoại tuyến và nhận biểu tượng",
"report_issue": "Báo cáo sự cố",
"view_source": "Xem mã nguồn",
"guess_distribution": "Phân bố dự đoán",
"games": "Trò chơi",
"win_percent": "% Thắng",
"streak": "Chuỗi",
"best": "Tốt nhất",
"all_languages": "Tất cả ngôn ngữ",
"languages": "Ngôn ngữ",
"play_more_languages": "Chơi nhiều ngôn ngữ hơn để xem thống kê tổng!",
"add_to_home": "Thêm vào màn hình chính",
"play_daily_like_app": "Chơi Wordle hàng ngày như ứng dụng",
"install": "Cài đặt",
"close": "đóng"
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Compare UI keys against default for Vietnamese.
python3 - <<'PY'
import json
import pathlib

root = pathlib.Path("webapp/data")
default_path = root / "default_language_config.json"
vi_path = root / "languages" / "vi" / "language_config.json"

if not default_path.exists():
    print(f"Error: {default_path} not found")
    exit(1)

if not vi_path.exists():
    print(f"Error: {vi_path} not found")
    exit(1)

default = json.loads(default_path.read_text(encoding="utf-8"))
vi = json.loads(vi_path.read_text(encoding="utf-8"))

def ui_keys(cfg):
    return set((cfg.get("ui") or {}).keys())

default_ui_keys = ui_keys(default)
vi_ui_keys = ui_keys(vi)

missing = sorted(default_ui_keys - vi_ui_keys)
extra = sorted(vi_ui_keys - default_ui_keys)

print(f"Default UI keys ({len(default_ui_keys)}): {sorted(default_ui_keys)}")
print(f"\nVietnamese UI keys ({len(vi_ui_keys)}): {sorted(vi_ui_keys)}")
print(f"\nMissing UI keys in Vietnamese ({len(missing)}):", missing)
print(f"Extra UI keys in Vietnamese ({len(extra)}):", extra)
PY

Repository: Hugo0/wordle

Length of output: 1653


Add missing 20 UI translation keys to Vietnamese language config.

The ui block is missing 20 keys present in the default config: about, avg_attempts, best_active_streak, best_overall_streak, best_streak, coming_soon, current_streak, external_links, game, games_lowercase, games_played, global_stats, languages_won, losses, no_games_yet, play, search_language, win_rate, wins, your_languages. These will surface English fallbacks in the Vietnamese UI. As per coding guidelines, ensure language_config.json contains required UI translations and metadata for each language.

🤖 Prompt for AI Agents
In `@webapp/data/languages/vi/language_config.json` around lines 35 - 62, The vi
language_config.json "ui" block is missing 20 UI keys causing English fallbacks;
add the missing keys into the "ui" object (e.g., about, avg_attempts,
best_active_streak, best_overall_streak, best_streak, coming_soon,
current_streak, external_links, game, games_lowercase, games_played,
global_stats, languages_won, losses, no_games_yet, play, search_language,
win_rate, wins, your_languages) with appropriate Vietnamese translations,
keeping key names exactly as listed so consumers (language_config.json -> "ui")
pick them up; ensure translations match the style of existing entries (short
labels and optional *_desc where applicable) and validate the JSON after
inserting the new fields.

@Hugo0 Hugo0 deleted the feature/dry-templates branch February 21, 2026 12:59
Hugo0 added a commit that referenced this pull request Feb 22, 2026
…ements (#111)

* refactor: DRY templates with Jinja2 partials

Extract shared HTML components into reusable Jinja2 partials to reduce
duplication between index.html and game.html templates.

## New Partials

- `_dark_mode_init.html` - Dark mode flash prevention script
- `_base_head.html` - Common head content (PWA meta, favicons, manifest, Vite CSS, GA)
- `_loading_skeleton.html` - Animated loading skeleton with pulsing tiles
- `_modal_backdrop.html` - Semi-transparent modal overlay
- `_pwa_install.html` - PWA install component
- `_toggle_switch.html` - Accessible button-based toggle switch macro

## Bug Fixes

- Added missing manifest link to homepage (was causing PWA install to fail)
- Homepage now has animated loading skeleton (was static text)
- Toggle switches now consistent across both pages (accessible button-based)

## Dev Experience

- Added `pnpm dev` script with dynamic template file watching
- gunicorn now auto-reloads on template changes
* style: Format app.py with black

* fix: Address CodeRabbit review feedback

- Include partials/*.html in dev script reload watching
- Use url_for for manifest path in _pwa_install.html
- Add type="button" and aria-label to toggle switch macro
- Add rel="noopener noreferrer" to external links in game.html
- Fix close button accessibility by moving click handler to button element
* feat: Use checkbox switch for native iOS haptics on toggles

Converted toggle switch from button to <input type="checkbox" switch>
which provides native haptic feedback on iOS Safari 17.4+. The visual
appearance remains the same but now users get tactile feedback when
toggling settings.
* feat: Major update - DRY templates, diacritics, word lists, UI improvements

Template refactoring:
- Extract shared partials: _base_head.html, _dark_mode_init.html,
  _loading_skeleton.html, _modal_backdrop.html, _pwa_install.html, _toggle_switch.html
- Use checkbox switch for native iOS haptics on toggle switches
- Improve accessibility: close buttons, aria-labels, rel="noopener noreferrer"
- Fix PWA install on homepage (add manifest link)

Diacritics support:
- Add diacritic matching for languages with accented characters
- New diacritics.ts module with normalization functions
- Keyboard hints showing which diacritics a key matches
- Tests for diacritic functionality

Word list improvements:
- Add blocklists for inappropriate words across 20+ languages
- Update word lists with curated selections
- Add word curation script (scripts/curate_words.py)
- Review files for upcoming daily words

UI/UX enhancements:
- Add OG images for social sharing
- Update language configs with new UI strings
- Keyboard layout improvements for multiple languages
- Style updates
* fix: Clean up language data and add xfail for keyboard gaps

Data fixes:
- sv: Convert 4,448 uppercase words to proper lowercase (including ä, ö, å)
- az: Remove malformed word "dəng:" with invalid colon
- mi: Add 'g' to character set for ng digraph support

Test improvements:
- Add KEYBOARD_COVERAGE_XFAIL for vi, ko, el (complex scripts needing expert review)
- All tests now pass (1636 passed, 248 skipped, 6 xfailed)
* fix: TypeScript errors and formatting issues

- Add non-null assertions in positional.ts for array access
- Run prettier to fix formatting in game.ts, index-app.ts, positional.ts
* feat: Add Playwright E2E tests and curated word schedule system

- Add Playwright E2E smoke tests (18 tests covering homepage, game pages, RTL, dark mode, mobile)
- Add curated_schedule.txt support for hand-picked daily words
- Create German (186 words) and Bulgarian (126 words) curated schedules
- Expand blocklists for Hebrew, Arabic, Turkish, Serbian
- Remove temporary review files and backups
- Update test_daily_word.py with comprehensive tests for new system
---------
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant