feat: Major update - DRY templates, diacritics, word lists, UI improvements#111
Conversation
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>
📝 WalkthroughWalkthroughConsolidates 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 Changes
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
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. Comment |
There was a problem hiding this comment.
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: Respectprefers-reduced-motionfor 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`; }
| "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)", |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
cat -n package.json | head -15Repository: 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)"
fiRepository: 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.
| "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`.
| {% 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> |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
| 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 |
There was a problem hiding this comment.
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.
- 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>
There was a problem hiding this comment.
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: Guardlanguage.key_diacritic_hintswhen absent.Accessing
.get()on a missingkey_diacritic_hintswill 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 %}
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
…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>
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
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>
There was a problem hiding this comment.
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):
copiednotification-word-not-validui section (11 keys):
aboutavg_attemptscoming_soonexternal_linksgameglobal_statslossesplaysearch_languagewin_ratewinsAdd 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 accesseslocalStoragewithout 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 intoggleDarkMode.The
toggleDarkMode()method accesseslocalStorage.setItemwithout 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
svandpau(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, buttest_language_config.pydoesn'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:pauword 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 missinglanguage.key_diacritic_hints.If a language doesn't define
key_diacritic_hints, calling.get()onNone/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.
FileNotFoundErroris covered, but a malformed or unreadable JSON will currently 500. Consider catchingjson.JSONDecodeError/PermissionErrorand 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 andhelp.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-abovealready inheritsflex-colfrom.has-hint.The
.key.has-hint.hint-aboverule only appliesflex-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-belowvariant in the future, consider removing this rule until needed, or add a differentiating style (e.g.,flex-col-reversefor 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.
normalizePositionalrebuilds 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:normalizePositionalCharduplicatestoRegularForm.Both functions have identical logic. Consider reusing
toRegularFormto 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 withClassVar.Per static analysis (RUF012), mutable class attributes should use
typing.ClassVarto 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 testscripts/curate_words.py (4)
32-43: Use explicitNoneunion in type hint.PEP 484 prohibits implicit
Optional. UseT | Nonesyntax 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
fprefix 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_idxandload_blocklistare duplicated acrosswebapp/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 usingtest.describe.eachfor parameterized language tests.Using a
forloop works buttest.describe.eachprovides 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.
| // 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; |
There was a problem hiding this comment.
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.
| 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); | ||
| } | ||
| }, |
There was a problem hiding this comment.
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.
| 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(); |
There was a problem hiding this comment.
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.
| # 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"] |
There was a problem hiding this comment.
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.
| 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 |
There was a problem hiding this comment.
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.
| "diacritic_map": { | ||
| "e": ["é", "è"], | ||
| "o": ["ó", "ò", "ô"] | ||
| }, |
There was a problem hiding this comment.
🧩 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 -20Repository: 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))
PYRepository: 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.jsonRepository: 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.jsonRepository: 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 -100Repository: 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.
| "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", "æ", "ø", "å", "⌫"] | ||
| ] | ||
| } |
There was a problem hiding this comment.
🧩 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"
fiRepository: 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
doneRepository: 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
doneRepository: 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.jsonRepository: 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.
| "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" |
There was a problem hiding this comment.
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.
| "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": "закрыть" |
There was a problem hiding this comment.
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.
| "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" |
There was a problem hiding this comment.
🧩 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)
PYRepository: 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.
…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 ---------
Summary
Major update combining multiple improvements:
Template Refactoring (DRY)
_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 hapticsAccessibility & Security
<input type="checkbox" switch>for native iOS haptic feedbacktype="button",aria-labelattributesrel="noopener noreferrer"Diacritics Support
diacritics.tsmodule with normalization functionsWord List Improvements
scripts/curate_words.py)UI/UX
Test Plan
pnpm test- TypeScript tests passpytest tests/- Python tests pass (expect pre-existing xfails)🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Improvements
Tests
Chores
✏️ Tip: You can customize this high-level summary in your review settings.