feat: Word definitions after game completion#121
Conversation
Add a definition card to the stats modal that shows the meaning of the daily word after win/loss. Fetches from native Wiktionary first, falls back to English Wiktionary REST API, then shows a "Look up" link. - New definitions.ts module with Wiktionary API client + fallback chain - "Show Definitions" toggle in Settings (default ON, persisted to localStorage) - Definition card with loading skeleton, dark mode support - Server-side AI image generation endpoint (DALL-E, behind OPENAI_API_KEY) - Images cached to disk, only generated for today's word - 7 new unit tests for definition fetching
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds Wiktionary-backed definitions, optional AI-generated word images, per-word anonymous stats, frontend UI toggles and rendering, new types and tests, server routes and disk caching for definitions/images/stats, a word page template, image pregeneration script, and related config/.gitignore updates. Changes
Sequence Diagram(s)sequenceDiagram
participant Client as Frontend Client
participant Game as Game Component
participant Def as Definitions Module
participant Native as Native Wiktionary
participant En as English Wiktionary
participant Server as Backend (app.py)
Client->>Game: Game ends / open Today tab
Game->>Game: check wordInfoEnabled / feedbackEnabled
alt Definitions enabled
Game->>Def: fetchDefinition(word, lang)
Def->>Native: query native Wiktionary
alt native returns
Native-->>Def: native definition (source:native)
else native empty/fail
Def->>En: query English Wiktionary
alt english returns
En-->>Def: english definition (source:english)
else english empty/fail
Def-->>Game: link-only fallback (source:link)
end
end
Def-->>Game: WordDefinition
Game->>Client: renderDefinitionCard / showDefinitionLoading
end
alt Word art enabled
Game->>Server: GET /<lang>/api/word-image/<word>
Server->>Server: validate, check disk cache
alt cached
Server-->>Game: cached image URL
else not cached and generation allowed
Server->>Server: build prompt, call OpenAI, save image
Server-->>Game: generated image URL
else not allowed
Server-->>Game: 404 / no image
end
Game->>Client: renderWordImage / showImageLoading
end
Game->>Server: POST /<lang>/api/word-stats (won, attempts)
Server->>Server: dedupe by IP, file-lock update stats
Server-->>Game: 204 (ack)
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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: 7
🧹 Nitpick comments (8)
webapp/templates/game.html (1)
397-398: LGTM — minor note ondisplay:nonevsv-show.The definition card uses
style="display: none;"for direct DOM manipulation by the definitions JS, rather than Vue'sv-show. This is functional but slightly inconsistent with the rest of the template. IfdefinitionsEnabledis already reactive Vue state ingame.ts, binding:styleorv-showhere would be more idiomatic and eliminate the need for explicit DOM manipulation indefinitions.ts.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@webapp/templates/game.html` around lines 397 - 398, Replace the inline style-based hide/show on the definition card with a Vue-controlled binding: remove style="display: none;" from the <div id="definition-card"> and use v-show="definitionsEnabled" (or bind :style="{ display: definitionsEnabled ? '' : 'none' }") so visibility is driven by the reactive definitionsEnabled state from game.ts; then update definitions.ts to stop manually toggling the DOM element and instead toggle the definitionsEnabled state (or emit an event) so Vue manages the element's display.tests/test_word_lists.py (1)
126-126: Annotate mutable class attribute withClassVar(Ruff RUF012).Ruff flags mutable default values on class attributes. Add
ClassVarto signal this is a class-level constant.♻️ Proposed fix
+from typing import ClassVar ... - KEYBOARD_COVERAGE_XFAIL: set[str] = {"ko", "de"} + KEYBOARD_COVERAGE_XFAIL: ClassVar[set[str]] = {"ko", "de"}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/test_word_lists.py` at line 126, Ruff warns that KEYBOARD_COVERAGE_XFAIL is a mutable class attribute; annotate it as a class-level constant by changing its type to use typing.ClassVar (i.e., KEYBOARD_COVERAGE_XFAIL: ClassVar[set[str]]), and add the necessary import for ClassVar from typing; update the declaration of KEYBOARD_COVERAGE_XFAIL accordingly so linters recognize it as a class var rather than a mutable instance attribute.tests/test_language_config.py (1)
156-156: Annotate mutable class attribute withClassVar(Ruff RUF012).Same pattern as
TestKeyboardCoverageintests/test_word_lists.py— annotate withClassVarand add a type annotation.♻️ Proposed fix
+from typing import ClassVar ... - KEYBOARD_COVERAGE_XFAIL = {"ko", "de"} + KEYBOARD_COVERAGE_XFAIL: ClassVar[set[str]] = {"ko", "de"}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/test_language_config.py` at line 156, Annotate the mutable class attribute KEYBOARD_COVERAGE_XFAIL as a ClassVar with an explicit type: change KEYBOARD_COVERAGE_XFAIL to something like KEYBOARD_COVERAGE_XFAIL: ClassVar[Set[str]] = {"ko", "de"} and ensure you import ClassVar and Set from typing (or typing_extensions if used elsewhere) so the mutable set is properly typed at class level (same pattern as TestKeyboardCoverage in tests/test_word_lists.py).webapp/app.py (2)
797-800: WastefulLanguageinstantiation just to readdaily_word.Line 798 constructs a full
Languageobject (builds keyboard layouts, diacritic hints, etc.) solely to readdaily_word. The module-level loaded data is sufficient to compute this directly.♻️ Proposed refactor
- # Only allow image generation for today's word - word_list = language_codes_5words[lang_code] - lang = Language(lang_code, word_list) - if word.lower() != lang.daily_word.lower(): + # Only allow image generation for today's word + todays_idx = get_todays_idx(language_configs[lang_code].get("timezone_offset", 0)) + word_list = language_codes_5words[lang_code] + blocklist = language_blocklists[lang_code] + if todays_idx <= MIGRATION_DAY_IDX: + daily_word = get_daily_word_legacy(word_list, set(), todays_idx) + else: + daily_word = _get_daily_word_for_lang(lang_code, todays_idx) # or inline the logic + if word.lower() != daily_word.lower():Alternatively, cache the
Languageinstances at module level per language (keyed by day index) so rebuilds are avoided.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@webapp/app.py` around lines 797 - 800, The code needlessly instantiates Language(lang_code, word_list) just to access lang.daily_word; avoid rebuilding heavy Language objects by computing the daily word directly from the module-level data (language_codes_5words and whatever day-index logic produces daily_word) or by introducing a module-level cache of Language instances keyed by (lang_code, day_index). Replace the direct Language(...) call in this block with either a direct lookup of the daily word using the same day-index algorithm used by Language.daily_word, or fetch the Language from the cache (e.g., get_cached_language(lang_code, day_index)) and then compare word.lower() to that cached.daily_word.lower().
782-800: No rate limiting — first request per day can trigger OpenAI image generation costs.The only abuse guard is the daily-word check (Line 799). Any user who observes today's word (e.g. after winning) can make repeated first-time requests across languages, and because the cache check only prevents re-generation for the same
(lang_code, word)tuple, a bad actor cycling through language codes could trigger many DALL-E generations per day at significant API cost.Consider adding at minimum a per-IP rate limit or requiring a server-side secret header for this endpoint.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@webapp/app.py` around lines 782 - 800, The word_image endpoint allows unauthenticated callers to trigger OpenAI image generation for today's word (via OPENAI_API_KEY) and is only protected by the daily-word check, so add server-side abuse protection by enforcing a per-IP rate limit or requiring a server-side secret header before any generation happens; update the word_image handler to check a rate limiter keyed by request.remote_addr (or validate a shared secret header like X-Internal-Secret) immediately after validating lang_code and before any image-generation/cache-miss path, and ensure the limiter state is stored server-side (in-memory with expiration, Redis, or Flask-Limiter) and returns 429 on excess requests to prevent cycling across language_codes_5words and repeated triggers for Language.daily_word.frontend/src/definitions.ts (1)
94-98: Optional: useDOMParserinstead of a detacheddivforstripHtmlSetting
innerHTMLon a detached element is safe in practice (scripts don't execute), butDOMParseris the explicitly documented safe API for parsing HTML to plain text.♻️ Suggested refactor
function stripHtml(html: string): string { - const div = document.createElement('div'); - div.innerHTML = html; - return div.textContent || div.innerText || ''; + return new DOMParser().parseFromString(html, 'text/html').body.textContent ?? ''; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/src/definitions.ts` around lines 94 - 98, Replace the detached div-based HTML stripping in the stripHtml function with DOMParser: parse the html string using new DOMParser().parseFromString(html, 'text/html'), then return the parsed document's textContent (or fallback to an empty string); update the function stripHtml to use this parsed document to extract plain text instead of creating a detached div and setting innerHTML.frontend/src/game.ts (1)
1284-1286: Add a.catch()to thefetchDefinitionpromise chain
fetchDefinitionis designed to never reject, but attaching a.catch()is defensive practice and avoids a potential unhandled rejection if the implementation ever changes.♻️ Suggested change
- fetchDefinition(this.todays_word, langCode).then((def) => { - renderDefinitionCard(def, container, uiStrings); - }); + fetchDefinition(this.todays_word, langCode) + .then((def) => { + renderDefinitionCard(def, container, uiStrings); + }) + .catch(() => { + // fetchDefinition should never reject, but clear loading state if it does + container.style.display = 'none'; + });🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/src/game.ts` around lines 1284 - 1286, The promise returned by fetchDefinition(this.todays_word, langCode) should have a .catch() to prevent unhandled rejections; update the chain that currently calls renderDefinitionCard(def, container, uiStrings) so that any error from fetchDefinition is caught, logged (e.g., console.error or app logger) and a safe fallback is used (for example call renderDefinitionCard with a null/empty def or hide the definition UI using container/uiStrings) to keep UI stable; ensure you modify the invocation site that references fetchDefinition, renderDefinitionCard, container, uiStrings, this.todays_word and langCode.frontend/src/__tests__/definitions.test.ts (1)
1-10: Consider adding tests forrenderDefinitionCardandshowDefinitionLoadingThe test suite covers
fetchDefinitionthoroughly, but the two exported rendering functions have no test coverage despite being part of the public API introduced in this PR.Would you like me to draft test cases for
renderDefinitionCard(link-only vs full definition card, partOfSpeech absent/present) andshowDefinitionLoading?🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/src/__tests__/definitions.test.ts` around lines 1 - 10, Add unit tests for the two uncovered exports: renderDefinitionCard and showDefinitionLoading. Create tests that assert renderDefinitionCard returns the expected DOM/HTML for both "link-only" (when only a URL/title are provided) and "full definition" variants, and check behavior when partOfSpeech is absent versus present (use the exported renderDefinitionCard function to render and compare strings or DOM nodes). Also add tests for showDefinitionLoading to verify it toggles/shows the loading UI as expected (call showDefinitionLoading(true/false) and assert the DOM contains/removes the loading indicator). Use the existing test file setup (import renderDefinitionCard and showDefinitionLoading alongside fetchDefinition, reuse vi.stubGlobal('fetch') and beforeEach) and create separate it blocks for each scenario.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@frontend/src/__tests__/definitions.test.ts`:
- Line 131: The test reads mockFetch.mock.calls[0][0] which can be undefined;
ensure the mock call exists before indexing: add an assertion like
expect(mockFetch).toHaveBeenCalled() (or check mockFetch.mock.calls.length > 0)
and then safely access the first call (e.g., use a non-null assertion or
destructure the first call) so that const url = mockFetch.mock.calls[0][0] as
string no longer triggers TS2532; update the test around the mockFetch usage in
definitions.test.ts to perform that presence check or assertion before using
mockFetch.mock.calls[0].
In `@frontend/src/definitions.ts`:
- Around line 239-258: The code currently interpolates external values into
container.innerHTML (posHtml, def.definition, def.word, def.url) which allows
XSS; add a small HTML-escape helper (e.g., escapeHtml) and use it on
def.partOfSpeech, the stripped def.definition, and def.word before
interpolation, and avoid injecting URLs via string HTML—set the anchor's
href/title via DOM properties or validate/encode def.url instead; alternatively
build the DOM with createElement/textContent for the word/definition and set
anchor.href to the sanitized URL rather than using container.innerHTML.
In `@frontend/src/game.ts`:
- Around line 1253-1268: The toggleDefinitions method should re-trigger a
definition load when the user turns definitions back on mid-session and the game
has already finished; inside toggleDefinitions (after the localStorage write and
analytics.trackSettingsChange), add a conditional that if
this.definitionsEnabled is true and this.gameOver is true then call
this.loadDefinition() (ensure it runs inside the existing this.$nextTick
callback so DOM state is settled). This uses the existing toggleDefinitions,
definitionsEnabled, gameOver, and loadDefinition symbols to locate where to add
the call.
- Around line 1263-1266: The call to analytics.trackSettingsChange uses setting
'definitions' but the SettingsChangeParams type (SettingsChangeParams) does not
include it; update the SettingsChangeParams union to include 'definitions' (so
the setting union becomes 'dark_mode' | 'haptics' | 'sound' | 'definitions') so
trackSettingsChange accepts this value; modify the type in analytics where
SettingsChangeParams is declared and ensure any related callers/types compile
after the change.
In `@webapp/app.py`:
- Line 832: The call to urllib.request.urlretrieve(image_url, cache_path) can
hang and doesn't validate the URL scheme; replace it by validating
image_url.scheme == "https" (or otherwise ensure http/https as allowed) then
open the URL with urllib.request.urlopen(image_url, timeout=...) and stream the
response to cache_path, handling exceptions (URLError, HTTPError,
socket.timeout) and closing the response; update any surrounding logic that
expects urlretrieve semantics (e.g., file creation) to write the response bytes
to cache_path and return/raise appropriate errors on failures.
- Around line 835-837: The current broad "except Exception as e" block should be
replaced with targeted exception handling: change the catch to only the expected
error types (e.g., IOError/OSError, ValueError, and any project-specific
ImageGenerationError used by the image generation routine) and handle those by
logging and returning the 500; do not swallow system exceptions—add an explicit
"except BaseException: raise" (or simply re-raise) to let
KeyboardInterrupt/MemoryError and other critical exceptions propagate. Locate
the existing "except Exception as e" in the image generation handler and update
it to "except (IOError, OSError, ValueError, ImageGenerationError) as e: ..."
and ensure any other exceptions are re-raised.
- Around line 804-834: The generated image URL from client.images.generate
(DALL·E 3) is PNG but the code saves it as .webp (cache_path built from
cache_dir and f"{word.lower()}.webp") and then serves it via
app.send_static_file, causing a content-type mismatch; fix by saving and serving
the file with the correct extension (e.g., change the cached filename from .webp
to .png when building cache_path and when calling app.send_static_file for the
cached file) or, if WebP is required, download the PNG (via
urllib.request.urlretrieve using image_url) and convert it to WebP using an
image library (e.g., Pillow) before writing and serving, ensuring
send_static_file and the stored filename reflect the actual output format.
---
Nitpick comments:
In `@frontend/src/__tests__/definitions.test.ts`:
- Around line 1-10: Add unit tests for the two uncovered exports:
renderDefinitionCard and showDefinitionLoading. Create tests that assert
renderDefinitionCard returns the expected DOM/HTML for both "link-only" (when
only a URL/title are provided) and "full definition" variants, and check
behavior when partOfSpeech is absent versus present (use the exported
renderDefinitionCard function to render and compare strings or DOM nodes). Also
add tests for showDefinitionLoading to verify it toggles/shows the loading UI as
expected (call showDefinitionLoading(true/false) and assert the DOM
contains/removes the loading indicator). Use the existing test file setup
(import renderDefinitionCard and showDefinitionLoading alongside
fetchDefinition, reuse vi.stubGlobal('fetch') and beforeEach) and create
separate it blocks for each scenario.
In `@frontend/src/definitions.ts`:
- Around line 94-98: Replace the detached div-based HTML stripping in the
stripHtml function with DOMParser: parse the html string using new
DOMParser().parseFromString(html, 'text/html'), then return the parsed
document's textContent (or fallback to an empty string); update the function
stripHtml to use this parsed document to extract plain text instead of creating
a detached div and setting innerHTML.
In `@frontend/src/game.ts`:
- Around line 1284-1286: The promise returned by
fetchDefinition(this.todays_word, langCode) should have a .catch() to prevent
unhandled rejections; update the chain that currently calls
renderDefinitionCard(def, container, uiStrings) so that any error from
fetchDefinition is caught, logged (e.g., console.error or app logger) and a safe
fallback is used (for example call renderDefinitionCard with a null/empty def or
hide the definition UI using container/uiStrings) to keep UI stable; ensure you
modify the invocation site that references fetchDefinition,
renderDefinitionCard, container, uiStrings, this.todays_word and langCode.
In `@tests/test_language_config.py`:
- Line 156: Annotate the mutable class attribute KEYBOARD_COVERAGE_XFAIL as a
ClassVar with an explicit type: change KEYBOARD_COVERAGE_XFAIL to something like
KEYBOARD_COVERAGE_XFAIL: ClassVar[Set[str]] = {"ko", "de"} and ensure you import
ClassVar and Set from typing (or typing_extensions if used elsewhere) so the
mutable set is properly typed at class level (same pattern as
TestKeyboardCoverage in tests/test_word_lists.py).
In `@tests/test_word_lists.py`:
- Line 126: Ruff warns that KEYBOARD_COVERAGE_XFAIL is a mutable class
attribute; annotate it as a class-level constant by changing its type to use
typing.ClassVar (i.e., KEYBOARD_COVERAGE_XFAIL: ClassVar[set[str]]), and add the
necessary import for ClassVar from typing; update the declaration of
KEYBOARD_COVERAGE_XFAIL accordingly so linters recognize it as a class var
rather than a mutable instance attribute.
In `@webapp/app.py`:
- Around line 797-800: The code needlessly instantiates Language(lang_code,
word_list) just to access lang.daily_word; avoid rebuilding heavy Language
objects by computing the daily word directly from the module-level data
(language_codes_5words and whatever day-index logic produces daily_word) or by
introducing a module-level cache of Language instances keyed by (lang_code,
day_index). Replace the direct Language(...) call in this block with either a
direct lookup of the daily word using the same day-index algorithm used by
Language.daily_word, or fetch the Language from the cache (e.g.,
get_cached_language(lang_code, day_index)) and then compare word.lower() to that
cached.daily_word.lower().
- Around line 782-800: The word_image endpoint allows unauthenticated callers to
trigger OpenAI image generation for today's word (via OPENAI_API_KEY) and is
only protected by the daily-word check, so add server-side abuse protection by
enforcing a per-IP rate limit or requiring a server-side secret header before
any generation happens; update the word_image handler to check a rate limiter
keyed by request.remote_addr (or validate a shared secret header like
X-Internal-Secret) immediately after validating lang_code and before any
image-generation/cache-miss path, and ensure the limiter state is stored
server-side (in-memory with expiration, Redis, or Flask-Limiter) and returns 429
on excess requests to prevent cycling across language_codes_5words and repeated
triggers for Language.daily_word.
In `@webapp/templates/game.html`:
- Around line 397-398: Replace the inline style-based hide/show on the
definition card with a Vue-controlled binding: remove style="display: none;"
from the <div id="definition-card"> and use v-show="definitionsEnabled" (or bind
:style="{ display: definitionsEnabled ? '' : 'none' }") so visibility is driven
by the reactive definitionsEnabled state from game.ts; then update
definitions.ts to stop manually toggling the DOM element and instead toggle the
definitionsEnabled state (or emit an event) so Vue manages the element's
display.
- New "Word Art" toggle in Settings (default ON) - Fetches AI-generated image via HEAD check, renders above definition - Loading skeleton while image loads, hides silently if unavailable - Separate from "Show Definitions" — users can enable/disable independently
✨ Finishing Touches
🧪 Generate unit tests (beta)
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 |
The English Wiktionary REST API returns definitions keyed by language
code (e.g., 'en', 'de') not language name ('English', 'German').
This was causing all definitions to fall through to the link fallback.
Also adds Word Art toggle in settings and word image container in
stats modal.
- Stats modal now has Today/Stats/Global tabs to reduce clutter - Today tab shows emoji board, word art, definition, share button - Stats tab shows guess distribution and language stats - DALL-E prompt now includes word definition for better images - Convert DALL-E output to WebP via Pillow for smaller file sizes - Fix: word art and definitions load independently (were coupled)
- New route /<lang>/word/<day_idx> shows each daily word's page - Server-side definition caching (Wiktionary) for SEO rendering - OG/Twitter meta tags with AI image as social preview - Anonymous stats collection (POST /api/word-stats) for difficulty data - Word page shows: tiles, AI image, definition, community stats, nav - Sitemap includes last 90 days of word pages per language - Fix sitemap.xml trailing 's' bug - Frontend submits game stats after win/loss (fire-and-forget) - Add Pillow dependency for WebP conversion - Add get_word_for_day() and idx_to_date() helper functions
- Fix: statsTab reset to 'today' after loading old localStorage state (old state had 'language' which matched no v-show condition) - Fix: Load .env file in app.py for local dev (gunicorn doesn't auto-load) - Fix: HEAD requests to word-image return 404 if not cached (don't trigger generation) - Refactor: Image loads via GET directly with onload/onerror handlers instead of HEAD check + separate img tag (simpler, works for first-time gen)
There was a problem hiding this comment.
Actionable comments posted: 7
🧹 Nitpick comments (4)
webapp/app.py (3)
874-893: Sitemap generates ~5,850+ URLs on every request.With 90 days × 65+ languages, each request to
/sitemap.xmlcomputesidx_to_dateand builds a large list. Consider caching this response (e.g., with a daily TTL) since the content only changes once per day.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@webapp/app.py` around lines 874 - 893, The sitemap handler currently recomputes a large word_pages list each request by calling get_todays_idx and idx_to_date for 90 days × many languages; change it to cache the fully rendered sitemap response (or the computed word_pages) with a daily TTL so it is only rebuilt once per day. Locate the sitemap generation that calls get_todays_idx, idx_to_date and render_template("sitemap.xml") and wrap the computation/rendering with a cache (e.g., Flask-Caching or an in-memory global cache keyed by date) that returns the cached Response (including Content-Type header) until the next day; ensure the cache key includes the current day index from get_todays_idx so it invalidates automatically when the date advances.
751-800: Definition cache has no TTL — stale definitions persist forever.
fetch_definition_cachedwrites to disk and never expires. If Wiktionary updates a definition or the initial fetch returned an empty result due to a transient error, the stale cache is served indefinitely. Consider adding a TTL check (e.g., re-fetch if cache file is older than 7 days).🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@webapp/app.py` around lines 751 - 800, fetch_definition_cached currently caches results on disk indefinitely; change it to respect a TTL (e.g., 7 days) by checking the cache file age before returning it: in fetch_definition_cached, inspect os.path.getmtime(cache_path) (or store a timestamp in the cached JSON) and compare to time.time() - TTL_SECONDS, and only return the cached JSON when it is fresh; if stale, proceed to re-fetch from Wiktionary, update the cache file with the new result (or empty object) and return the fresh result; ensure cache write still creates cache_dir and gracefully handles I/O errors and treat an empty cached object as no definition (None) so transient empty errors get retried after TTL.
420-439:get_word_for_dayduplicatesLanguage._get_daily_wordlogic — drift risk.This standalone function mirrors the algorithm in
Language._get_daily_word()(lines 617–651). If either copy changes without updating the other, different words will be served on the game page vs. the word page. Consider extracting a shared function that both call.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@webapp/app.py` around lines 420 - 439, get_word_for_day duplicates the selection logic in Language._get_daily_word, which risks drift; extract the shared selection algorithm into a single helper (e.g., _select_daily_word or similar) that encapsulates the MIGRATION_DAY_IDX branching, curated_schedule/daily_words checks and calls to get_daily_word_legacy/get_daily_word_consistent_hash using the same parameters (word_list, blocklist, daily_words, curated_schedule, day_idx, lang_code), then have both get_word_for_day and Language._get_daily_word call that helper so behavior remains identical and duplication is removed.pyproject.toml (1)
12-13: Consider makingopenaiandpillowoptional dependencies.Both packages are only used for the AI image generation feature (the
word_image()function), which is silently disabled whenOPENAI_API_KEYis not set. Making them hard requirements inflates the install footprint (especiallyopenaiwith its transitive dependencies like httpx and pydantic) for all deployments. You can move them to an optional dependency group:♻️ Suggested pyproject.toml change
dependencies = [ "flask>=3.1.0", "flask-cors>=6.0.0", "gunicorn>=24.0.0", "Flask-FlatPages>=0.9.0", "nltk>=3.9.0", - "openai>=2.21.0", - "pillow>=12.1.1", ] + +[project.optional-dependencies] +ai = [ + "openai>=2.21.0", + "pillow>=12.1.1", +]The code already supports this: Pillow has an explicit
except ImportErrorfallback (with graceful degradation to raw PNG), and the openai import is wrapped in a try/except that returns a 500 error on failure. Both patterns indicate these dependencies were intended to be optional.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@pyproject.toml` around lines 12 - 13, Move the hard requirement entries for "openai" and "pillow" out of the top-level dependencies in pyproject.toml and add them to an optional dependency group (e.g., [project.optional-dependencies] image = ["openai>=2.21.0","pillow>=12.1.1"]) so they become installable via extras (pip install .[image]); keep the runtime behavior unchanged (word_image() already handles ImportError and the OPENAI_API_KEY gating), and update any packaging metadata or docs to mention the new optional "image" extra so users know to install it when they need AI image generation.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@frontend/src/game.ts`:
- Around line 1310-1341: The promise chains in loadDefinition (function
loadDefinition) call fetchDefinition(...).then(...) and
fetchWordImage(...).then(...) without .catch(), so rejections from fetch or from
renderDefinitionCard/renderWordImage can be unhandled; update these chains to
add .catch() handlers that safely handle errors (e.g., log the error, hide or
clear the corresponding container element returned by
document.getElementById('definition-card') / 'word-image-card', and avoid
throwing), or alternatively wrap the render calls in try/catch inside the
.then() and handle exceptions similarly; ensure the fix references
loadDefinition, fetchDefinition, fetchWordImage, renderDefinitionCard and
renderWordImage so the error handling is added for both flows.
In `@webapp/app.py`:
- Around line 823-832: _saving word stats currently lacks synchronization
causing lost updates: update _save_word_stats and _load_word_stats to use
OS-level file locking (e.g., fcntl.flock) around read-modify-write operations
and perform atomic writes by writing to a temp file then os.replace/rename to
the target path; ensure the lock is held while reading and writing the stats
JSON (use the same lock file/path for both functions). Also address the
per-process _stats_seen_ips limitation by moving that state to a shared store
(e.g., Redis) or by coordinating via the same file lock so concurrent Gunicorn
workers don’t overwrite each other.
- Around line 1064-1113: The submit_word_stats handler fails to validate
attempts early and risks corrupting the stats file under concurrent requests;
update submit_word_stats to validate that attempts is an int in 1..6 before
counting a win (use the variables day_idx, attempts, won) and ensure saving is
atomic by changing _save_word_stats/_load_word_stats usage to perform a
load-modify-save protected by a file lock or by writing to a temp file and
os.replace() (or use a cross-process lock like portalocker) so concurrent
workers don't clobber the JSON; also avoid the Ruff TRY300 by moving the final
return "", 200 into the normal control flow (else branch) instead of after the
try, and keep the in-memory dedup map _stats_seen_ips behavior noted (it does
not dedupe across Gunicorn workers).
- Around line 932-936: The code is instantiating Language(lang_code, word_list)
just to check today's word and unnecessarily loads heavy data; replace that by
calling the existing get_word_for_day(lang_code, word_list) (or the helper
function that returns the daily word) and compare word.lower() to that result
instead of creating a Language instance; remove the Language(...) construction
and use get_word_for_day(lang_code, language_codes_5words[lang_code]) for the
comparison.
- Around line 807-808: The _stats_seen_ips dict retains every "lang:day:ip" key
forever; change the implementation so the store is bounded or rotated daily:
replace _stats_seen_ips with a size-limited LRU cache or a dict keyed by day
(e.g., stats_seen_by_day) and clear/remove entries for previous days at
midnight, and ensure the lookup/insert sites use the new structure; reference
the existing symbol _stats_seen_ips and the "lang:day:ip" key format when making
the changes.
In `@webapp/templates/word.html`:
- Around line 38-43: The header contains hardcoded English strings ("Play
Wordle", the H1 prefix, and the date format) which break localization; update
the template to pull these from the same localization object used elsewhere
(e.g., use config.ui.play_wordle, config.ui.wordle_title or similar keys) with
safe fallbacks, e.g. replace the link text, the H1 prefix around {{
lang_name_native }} and any surrounding static labels with {{ config.ui.<key> |
default('Play Wordle') }}-style expressions and keep the dynamic values ({{
lang_code }}, {{ lang_name_native }}, {{ day_idx }}, {{ word_date.strftime('%B
%d, %Y') }}) intact so the strings are localized consistently.
- Around line 169-185: The shareWord() function interpolates Jinja variables
directly into a JS string which can break on quotes/backslashes; update the
template to emit safe JS values by JSON-escaping the Jinja variables (e.g., use
{{ lang_name_native | tojson }}, {{ word | tojson }}, {{ lang_code | tojson }},
{{ day_idx | tojson }}) or assign them to JS variables using tojson and then
build the text string from those variables so special characters are properly
escaped before using navigator.share or clipboard APIs.
---
Duplicate comments:
In `@frontend/src/__tests__/definitions.test.ts`:
- Line 131: The test fails with TS2532 because mockFetch.mock.calls[0] can be
undefined; update the access in the test (where you build url from
mockFetch.mock.calls[0][0]) to use optional chaining on mockFetch.mock.calls and
its first element (and cast/result appropriately) so the expression safely
handles undefined calls and satisfies TypeScript (adjust the assertion to
account for a possibly undefined call if needed).
In `@frontend/src/definitions.ts`:
- Around line 162-203: The renderDefinitionCard function currently interpolates
untrusted Wiktionary fields (def.partOfSpeech, def.word, def.definition,
def.url) directly into container.innerHTML causing XSS and unsafe hrefs; fix it
by building DOM nodes programmatically (createElement, textContent) or
HTML-escaping those fields before injecting, and validate/sanitize def.url to
allow only http/https schemes (reject or transform javascript: and other
dangerous schemes) before setting as an anchor href; update the branches where
container.innerHTML is set (the link-only branch and the detailed-definition
branch) to use safe DOM creation and validated URLs instead of raw string
interpolation.
In `@frontend/src/game.ts`:
- Around line 1275-1278: Update the SettingsChangeParams type used by
analytics.trackSettingsChange to include the missing options; specifically,
modify the union for the setting field in the SettingsChangeParams declaration
so it becomes 'dark_mode' | 'haptics' | 'sound' | 'definitions' | 'word_art' so
calls to analytics.trackSettingsChange (e.g., in game.ts where definitions and
word_art are tracked) compile successfully.
- Around line 1265-1280: The toggleDefinitions() handler needs to re-trigger the
definition load when definitions are turned back on after a finished game: after
persisting definitionsEnabled and calling analytics.trackSettingsChange, add a
guard that if this.definitionsEnabled is true and the game is already finished
(check your game-state flag such as this.gameComplete / this.gameOver /
this.isGameComplete), call this.loadDefinition() (ensure loadDefinition exists
before invoking to avoid errors) so the definition card is re-rendered when
toggled on post-game.
In `@webapp/app.py`:
- Around line 995-1009: The download/conversion path should use a timeout and
preserve correct file type: replace the blocking urlreq.urlretrieve call with
urlreq.urlopen(image_url, timeout=30) and stream the response into the
NamedTemporaryFile (use shutil.copyfileobj or equivalent) before conversion; in
the except ImportError fallback do not rename a raw PNG to a .webp
filename—instead move tmp_path to a cache path whose extension matches the
actual PNG bytes (or derive fallback_cache_path = cache_path with ".png"
extension) so browsers receive correct content, and ensure tmp_path is cleaned
up or nulled after move; update references to urlreq.urlretrieve,
urlreq.urlopen, NamedTemporaryFile, Image.open, except ImportError, cache_path
and tmp_path accordingly.
---
Nitpick comments:
In `@pyproject.toml`:
- Around line 12-13: Move the hard requirement entries for "openai" and "pillow"
out of the top-level dependencies in pyproject.toml and add them to an optional
dependency group (e.g., [project.optional-dependencies] image =
["openai>=2.21.0","pillow>=12.1.1"]) so they become installable via extras (pip
install .[image]); keep the runtime behavior unchanged (word_image() already
handles ImportError and the OPENAI_API_KEY gating), and update any packaging
metadata or docs to mention the new optional "image" extra so users know to
install it when they need AI image generation.
In `@webapp/app.py`:
- Around line 874-893: The sitemap handler currently recomputes a large
word_pages list each request by calling get_todays_idx and idx_to_date for 90
days × many languages; change it to cache the fully rendered sitemap response
(or the computed word_pages) with a daily TTL so it is only rebuilt once per
day. Locate the sitemap generation that calls get_todays_idx, idx_to_date and
render_template("sitemap.xml") and wrap the computation/rendering with a cache
(e.g., Flask-Caching or an in-memory global cache keyed by date) that returns
the cached Response (including Content-Type header) until the next day; ensure
the cache key includes the current day index from get_todays_idx so it
invalidates automatically when the date advances.
- Around line 751-800: fetch_definition_cached currently caches results on disk
indefinitely; change it to respect a TTL (e.g., 7 days) by checking the cache
file age before returning it: in fetch_definition_cached, inspect
os.path.getmtime(cache_path) (or store a timestamp in the cached JSON) and
compare to time.time() - TTL_SECONDS, and only return the cached JSON when it is
fresh; if stale, proceed to re-fetch from Wiktionary, update the cache file with
the new result (or empty object) and return the fresh result; ensure cache write
still creates cache_dir and gracefully handles I/O errors and treat an empty
cached object as no definition (None) so transient empty errors get retried
after TTL.
- Around line 420-439: get_word_for_day duplicates the selection logic in
Language._get_daily_word, which risks drift; extract the shared selection
algorithm into a single helper (e.g., _select_daily_word or similar) that
encapsulates the MIGRATION_DAY_IDX branching, curated_schedule/daily_words
checks and calls to get_daily_word_legacy/get_daily_word_consistent_hash using
the same parameters (word_list, blocklist, daily_words, curated_schedule,
day_idx, lang_code), then have both get_word_for_day and
Language._get_daily_word call that helper so behavior remains identical and
duplication is removed.
| <header class="text-center mb-6"> | ||
| <a href="/{{ lang_code }}" class="text-sm text-blue-500 hover:text-blue-600 dark:text-blue-400 dark:hover:text-blue-300"> | ||
| ← Play Wordle {{ lang_name_native }} | ||
| </a> | ||
| <h1 class="text-2xl font-bold mt-2">Wordle {{ lang_name_native }} #{{ day_idx }}</h1> | ||
| <p class="text-sm text-neutral-500 dark:text-neutral-400">{{ word_date.strftime('%B %d, %Y') }}</p> |
There was a problem hiding this comment.
Hardcoded English strings break localization for non-English languages.
Strings like "Play Wordle", "Share", "Community Stats", "Players", "Win Rate", "Avg Attempts", "Today", and "Play Today's Wordle" are hardcoded in English throughout this template. The game template uses config.ui.* keys with fallbacks. For consistency and to support the 65+ languages, use the same localization pattern here:
- ← Play Wordle {{ lang_name_native }}
+ ← {{ config.ui.play or "Play" }} Wordle {{ lang_name_native }}(Apply similarly for other hardcoded strings.)
📝 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.
| <header class="text-center mb-6"> | |
| <a href="/{{ lang_code }}" class="text-sm text-blue-500 hover:text-blue-600 dark:text-blue-400 dark:hover:text-blue-300"> | |
| ← Play Wordle {{ lang_name_native }} | |
| </a> | |
| <h1 class="text-2xl font-bold mt-2">Wordle {{ lang_name_native }} #{{ day_idx }}</h1> | |
| <p class="text-sm text-neutral-500 dark:text-neutral-400">{{ word_date.strftime('%B %d, %Y') }}</p> | |
| <header class="text-center mb-6"> | |
| <a href="/{{ lang_code }}" class="text-sm text-blue-500 hover:text-blue-600 dark:text-blue-400 dark:hover:text-blue-300"> | |
| ← {{ config.ui.play or "Play" }} Wordle {{ lang_name_native }} | |
| </a> | |
| <h1 class="text-2xl font-bold mt-2">Wordle {{ lang_name_native }} #{{ day_idx }}</h1> | |
| <p class="text-sm text-neutral-500 dark:text-neutral-400">{{ word_date.strftime('%B %d, %Y') }}</p> |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@webapp/templates/word.html` around lines 38 - 43, The header contains
hardcoded English strings ("Play Wordle", the H1 prefix, and the date format)
which break localization; update the template to pull these from the same
localization object used elsewhere (e.g., use config.ui.play_wordle,
config.ui.wordle_title or similar keys) with safe fallbacks, e.g. replace the
link text, the H1 prefix around {{ lang_name_native }} and any surrounding
static labels with {{ config.ui.<key> | default('Play Wordle') }}-style
expressions and keep the dynamic values ({{ lang_code }}, {{ lang_name_native
}}, {{ day_idx }}, {{ word_date.strftime('%B %d, %Y') }}) intact so the strings
are localized consistently.
There was a problem hiding this comment.
Acknowledged — word.html is a lightweight SEO-focused subpage. Full i18n for this template is a follow-up task. The key content (word, definition) is already localized via the language config.
There was a problem hiding this comment.
Rate Limit Exceeded
@Hugo0 have exceeded the limit for the number of chat messages per hour. Please wait 1 minutes and 29 seconds before sending another message.
…script - Remove loading="lazy" from detached img element — browsers won't load lazy images that aren't in the DOM, causing the skeleton to never resolve - Set img.src last to ensure onload/onerror handlers are attached first - Add scripts/pregenerate_images.py for daily cron image pre-generation
- Word-image endpoint now accepts ?day_idx= param to verify historical words - Word subpage loads images on-demand via endpoint (not just pre-cached) - OG/Twitter image tags point to endpoint URL for social preview generation - Image hidden with onerror if generation fails or API key not configured
- Word page returns 404 for today's word (day_idx >= todays_idx) - Nav "next" link stops at yesterday, shows "Latest" label - Sitemap excludes today's word pages - Fix pregenerate script working directory for webapp imports
There was a problem hiding this comment.
Actionable comments posted: 5
🧹 Nitpick comments (5)
scripts/pregenerate_images.py (2)
48-64: Silent failure masks definition fetch problems.The bare
except Exception: passon line 62 swallows all errors including network misconfigurations. For a batch script meant to be run via cron, logging the error (even to stderr) would help diagnose failures.Suggested improvement
- except Exception: - pass + except Exception as e: + print(f" [warn] Definition fetch failed for {word} ({lang_code}): {e}", file=sys.stderr) return None🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@scripts/pregenerate_images.py` around lines 48 - 64, The function fetch_definition currently swallows all errors with a bare except, making failures silent; modify fetch_definition to catch Exception as e and log the exception (including the word and lang_code) to stderr or via the logging module before returning None so batch/cron runs surface network or parsing issues; ensure the logging call references fetch_definition context and preserves the existing return behavior.
26-31:os.chdir()at module import time is a global side effect.Line 31 changes the process working directory, which could affect any code that imports this module or runs after it. Consider using explicit absolute paths instead, or scoping the
chdirinsidemain().🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@scripts/pregenerate_images.py` around lines 26 - 31, The module-level os.chdir("_project_root/webapp") is a harmful global side effect; remove the top-level os.chdir call and instead compute and use explicit absolute paths (using _script_dir and _project_root) for any file operations or imports, or move the chdir into a guarded main() function so it only runs when the script is executed directly; update code that relied on relative paths to use os.path.join(_project_root, "webapp", ...) and keep the existing sys.path.insert(0, _project_root) unchanged so imports still work without changing the process working directory.frontend/src/definitions.ts (1)
91-131: Native Wiktionary fetch constructs a very long query URL inline — consider extracting parameters.This is a minor readability nit. The URL on line 96 packs many query parameters into a single string. Consider building it with
URLSearchParamsfor clarity and to avoid encoding issues.Example
const params = new URLSearchParams({ action: 'query', titles: word, prop: 'extracts', explaintext: '1', exintro: '1', format: 'json', origin: '*', }); const url = `https://${wikiLang}.wiktionary.org/w/api.php?${params}`;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/src/definitions.ts` around lines 91 - 131, The inline construction of the Wiktionary query string in fetchFromNativeWiktionary makes the URL hard to read and risks encoding mistakes; replace the concatenated query string with a URLSearchParams object (e.g., build params with keys action, titles, prop, explaintext, exintro, format, origin) and use `?${params}` to form the final url while still using getWiktionaryLang(word) for the host and keep returning the same object shape (word, definition, source: 'native', url: getWiktionaryUrl(word, lang)); ensure titles uses the raw word (URLSearchParams will handle encoding) and keep the existing error handling and truncation logic intact.frontend/src/game.ts (1)
866-881:Object.assign(this, data)resetsstatsTab— verify no other transient state is affected.After loading saved game state via
Object.assign,statsTabis explicitly reset to'today'(line 876). SinceSavedGameStatedoesn't includedefinitionsEnabledorwordArtEnabled, those preferences (loaded earlier increated()) survive theObject.assign. However, ifSavedGameStateis ever extended with conflicting keys, this could silently overwrite runtime state.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/src/game.ts` around lines 866 - 881, The loadFromLocalStorage() method uses Object.assign(this, data) which can overwrite transient runtime state (e.g., statsTab, definitionsEnabled, wordArtEnabled) if SavedGameState later adds conflicting keys; update loadFromLocalStorage() to only copy persistent fields instead of assigning the whole data object — for example, map/assign individual persisted properties from the parsed SavedGameState into the instance or whitelist keys to merge, then explicitly reset statsTab = 'today'; reference loadFromLocalStorage(), Object.assign(this, data), SavedGameState, statsTab, and the created() initialization of definitionsEnabled/wordArtEnabled when making the change.webapp/app.py (1)
885-901: Sitemap regenerated on every request for 65+ languages × 90 days.
site_map()computes ~5,850+ word page entries on every request. Consider caching the rendered XML (e.g., with a TTL of a few hours or regenerating once per day) to avoid repeated work.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@webapp/app.py` around lines 885 - 901, The sitemap is being regenerated on every request (iterating language_codes × 90 days) inside the site_map handler; add a cache so the heavy work runs only periodically: in site_map() (or near render_template("sitemap.xml") / make_response) store a module-level cached_response plus a cached_timestamp or cached_date, and on each request return the cached_response if it is still fresh (e.g., same day or TTL of a few hours); otherwise rebuild word_pages using get_todays_idx()/language_codes, re-render with render_template("sitemap.xml"), update cached_response and timestamp, and return it.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@frontend/src/game.ts`:
- Around line 1294-1309: The SettingsChangeParams type used by
analytics.trackSettingsChange is missing 'definitions' and 'word_art' keys;
update the SettingsChangeParams.setting union type (in the analytics type
definition) to include those two values so it becomes: 'dark_mode' | 'haptics' |
'sound' | 'definitions' | 'word_art', keeping the rest of the type structure
intact and ensuring any callers like analytics.trackSettingsChange(...) compile
without error.
In `@scripts/pregenerate_images.py`:
- Around line 124-142: urllib.request.urlretrieve blocks indefinitely and
doesn't support timeouts; replace its usage in the download block (where
tmp_path is set) with a urlopen/read pattern that supplies a timeout and writes
the bytes to tmp_path, then continue to use PIL's Image.open(tmp_path) to save
WebP to cache_path; in the ImportError fallback (currently using
shutil.move(tmp_path, cache_path)), do not silently save PNG bytes with a .webp
name—either change cache_path to a .png extension before moving or move to a new
file name (e.g., cache_path_png) and ensure any downstream consumers expect
.png, so update the variable used by the rest of the function accordingly (refer
to tmp_path, cache_path, Image.open, shutil.move, and the PIL ImportError
handler).
In `@webapp/app.py`:
- Around line 986-990: The variable lang_name is assigned from
language_configs[lang_code].get("name", lang_code) but never used, causing an
unused-variable warning; either remove that assignment if the human-readable
name isn't needed, or use lang_name where you currently embed lang_code in the
prompt/metadata so the user-facing text uses the friendly name—update the prompt
construction (the code that builds or formats the prompt near where client =
openai.OpenAI(...) is created) to refer to lang_name instead of lang_code, or
pass lang_name into the prompt/template builder that consumes language
information.
- Around line 20-28: The current .env loader reads _val via _line.partition("=")
and sets it into the environment with os.environ.setdefault without removing
surrounding quotes, so values like OPENAI_API_KEY="sk-abc" keep the quotes;
update the logic that computes _val (referencing _line and _env_path) to strip
whitespace and then remove matching surrounding single or double quotes from the
value before calling os.environ.setdefault (handle empty values and mismatched
quotes gracefully); alternatively replace this loader with python-dotenv if you
prefer a drop-in robust parser.
- Around line 967-983: The inline Wiktionary fetch duplicates logic already
implemented in fetch_definition_cached; replace the try/except block that builds
def_url and parses def_data with a call to fetch_definition_cached(word,
lang_code) (or appropriate signature) and assign its returned string to
definition_hint (falling back to ""), preserving the existing behavior
(timeout/optional nature) and keeping the surrounding comment; ensure you handle
None/empty returns the same way the current code does and remove the duplicated
parsing code (the block that sets raw_def/clean_def/definition_hint).
---
Duplicate comments:
In `@frontend/src/definitions.ts`:
- Around line 170-203: The templates use container.innerHTML with external
fields (def.word, def.definition, def.partOfSpeech, def.url) causing XSS; add an
HTML-escape helper (e.g. escapeHtml(value:string):string) and run def.word,
def.definition, def.partOfSpeech and def.url through it before interpolating
into the template strings used by container.innerHTML (and replace posHtml
construction to use the escaped partOfSpeech). Ensure the helper is used
wherever these def.* values are inserted into the DOM in this file so raw
untrusted text is never injected as HTML.
In `@frontend/src/game.ts`:
- Around line 1266-1281: The build breaks because analytics.trackSettingsChange
in toggleDefinitions passes setting: 'definitions' which isn't included in the
SettingsChangeParams union; update the SettingsChangeParams type (in
frontend/src/analytics.ts) to include the literal 'definitions' in the union so
that the call from toggleDefinitions() and other callers type-checks, and then
re-run type checks to ensure no other callers need adjustments.
- Around line 1311-1326: The promise chain in loadDefinition uses
fetchDefinition(...).then(...) but lacks a .catch(), so any rejection from
fetchDefinition or renderDefinitionCard will be unhandled; update loadDefinition
to append a .catch(...) to the fetchDefinition(...) promise that calls the same
DOM cleanup used on success (e.g., hide or remove the 'definition-card'
container or call a helper such as hideDefinitionLoading) and logs the error
(using console.error or the app logger) to avoid silent failures and ensure the
container is hidden when fetch or render fails.
- Around line 1266-1281: toggleDefinitions currently updates localStorage and
analytics but doesn't re-load definitions when the user turns definitions back
on mid-session; inside toggleDefinitions (the this.$nextTick callback) detect
when this.definitionsEnabled becomes true and call the existing loadDefinition()
method (ensure you call it after updating localStorage/analytics so it runs in
the same nextTick flow), so toggling ON re-invokes loadDefinition().
In `@webapp/app.py`:
- Around line 818-819: _stats_seen_ips currently grows without bounds because
each unique "lang:day:ip" key is kept for process lifetime; replace the plain
dict with a bounded cache that evicts old entries (e.g., an LRU or TTL cache) or
add timestamps + periodic cleanup to expire entries. Specifically, change the
global _stats_seen_ips usage to a cache implementation (or wrap accesses in
functions like mark_seen()/is_seen() that use cachetools.TTLCache or an
OrderedDict-based LRU) with a sensible maxsize and/or ttl so entries for past
days are removed automatically and memory cannot grow unbounded.
- Around line 1012-1036: The urlreq.urlretrieve call in the image download flow
(urlreq.urlretrieve) needs a timeout to avoid blocking the Flask request thread
and the ImportError fallback currently moves a PNG to cache_path and then
returns via app.send_static_file(... .webp) which causes a mismatched
content/extension; fix by using a network call with a timeout (or
urllib.request.urlopen with timeout) to download into tmp_path, handle network
errors and timeouts to return a 504/appropriate error, and change the PIL
fallback in the except ImportError branch (where Image.open is used and
_shutil.move(tmp_path, cache_path) is called) to convert or re-encode the PNG
bytes to a proper WebP file (or alternatively save with the original .png
extension and update the response) so that cache_path actually contains a WebP
before calling
app.send_static_file(f"word-images/{lang_code}/{word.lower()}.webp"); ensure
tmp_path cleanup logic remains intact.
- Around line 834-843: Concurrent writes can corrupt the JSON; update
_save_word_stats and the corresponding _load_word_stats to use an inter-process
file lock and atomic replace: acquire an exclusive lock on stats_path (or a
.lock beside it) before reading or writing, for save write to a temp file in
stats_dir (e.g., stats_path + ".tmp"), fsync the temp file, then
os.replace(temp, stats_path) to atomically swap in the new file, and release the
lock; for load acquire a shared or exclusive lock around opening and json.load
to ensure consistent reads. Ensure the functions reference stats_path and
stats_dir when creating the temp file and lock. Use a cross-platform file-lock
library (e.g., portalocker/lockfile) or fcntl on Unix.
---
Nitpick comments:
In `@frontend/src/definitions.ts`:
- Around line 91-131: The inline construction of the Wiktionary query string in
fetchFromNativeWiktionary makes the URL hard to read and risks encoding
mistakes; replace the concatenated query string with a URLSearchParams object
(e.g., build params with keys action, titles, prop, explaintext, exintro,
format, origin) and use `?${params}` to form the final url while still using
getWiktionaryLang(word) for the host and keep returning the same object shape
(word, definition, source: 'native', url: getWiktionaryUrl(word, lang)); ensure
titles uses the raw word (URLSearchParams will handle encoding) and keep the
existing error handling and truncation logic intact.
In `@frontend/src/game.ts`:
- Around line 866-881: The loadFromLocalStorage() method uses
Object.assign(this, data) which can overwrite transient runtime state (e.g.,
statsTab, definitionsEnabled, wordArtEnabled) if SavedGameState later adds
conflicting keys; update loadFromLocalStorage() to only copy persistent fields
instead of assigning the whole data object — for example, map/assign individual
persisted properties from the parsed SavedGameState into the instance or
whitelist keys to merge, then explicitly reset statsTab = 'today'; reference
loadFromLocalStorage(), Object.assign(this, data), SavedGameState, statsTab, and
the created() initialization of definitionsEnabled/wordArtEnabled when making
the change.
In `@scripts/pregenerate_images.py`:
- Around line 48-64: The function fetch_definition currently swallows all errors
with a bare except, making failures silent; modify fetch_definition to catch
Exception as e and log the exception (including the word and lang_code) to
stderr or via the logging module before returning None so batch/cron runs
surface network or parsing issues; ensure the logging call references
fetch_definition context and preserves the existing return behavior.
- Around line 26-31: The module-level os.chdir("_project_root/webapp") is a
harmful global side effect; remove the top-level os.chdir call and instead
compute and use explicit absolute paths (using _script_dir and _project_root)
for any file operations or imports, or move the chdir into a guarded main()
function so it only runs when the script is executed directly; update code that
relied on relative paths to use os.path.join(_project_root, "webapp", ...) and
keep the existing sys.path.insert(0, _project_root) unchanged so imports still
work without changing the process working directory.
In `@webapp/app.py`:
- Around line 885-901: The sitemap is being regenerated on every request
(iterating language_codes × 90 days) inside the site_map handler; add a cache so
the heavy work runs only periodically: in site_map() (or near
render_template("sitemap.xml") / make_response) store a module-level
cached_response plus a cached_timestamp or cached_date, and on each request
return the cached_response if it is still fresh (e.g., same day or TTL of a few
hours); otherwise rebuild word_pages using get_todays_idx()/language_codes,
re-render with render_template("sitemap.xml"), update cached_response and
timestamp, and return it.
| toggleWordArt(): void { | ||
| this.$nextTick(() => { | ||
| try { | ||
| localStorage.setItem( | ||
| 'wordArtEnabled', | ||
| this.wordArtEnabled ? 'true' : 'false' | ||
| ); | ||
| } catch { | ||
| // localStorage unavailable | ||
| } | ||
| analytics.trackSettingsChange({ | ||
| setting: 'word_art', | ||
| value: this.wordArtEnabled, | ||
| }); | ||
| }); | ||
| }, |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
rg -n 'SettingsChangeParams' frontend/src/analytics.ts -A5Repository: Hugo0/wordle
Length of output: 407
🏁 Script executed:
rg -n "setting.*definitions|'definitions'" frontend/src/game.tsRepository: Hugo0/wordle
Length of output: 108
🏁 Script executed:
sed -n '1270,1285p' frontend/src/game.tsRepository: Hugo0/wordle
Length of output: 664
Update SettingsChangeParams type to include missing setting keys.
The trackSettingsChange() calls in game.ts use 'definitions' and 'word_art' settings, but these are not included in the SettingsChangeParams.setting type definition in frontend/src/analytics.ts. Extend the union type to:
setting: 'dark_mode' | 'haptics' | 'sound' | 'definitions' | 'word_art';🧰 Tools
🪛 GitHub Check: Build Check
[failure] 1305-1305:
Type '"word_art"' is not assignable to type '"dark_mode" | "haptics" | "sound"'.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@frontend/src/game.ts` around lines 1294 - 1309, The SettingsChangeParams type
used by analytics.trackSettingsChange is missing 'definitions' and 'word_art'
keys; update the SettingsChangeParams.setting union type (in the analytics type
definition) to include those two values so it becomes: 'dark_mode' | 'haptics' |
'sound' | 'definitions' | 'word_art', keeping the rest of the type structure
intact and ensuring any callers like analytics.trackSettingsChange(...) compile
without error.
There was a problem hiding this comment.
This was about the settings toggle analytics — the code correctly tracks the setting change. No issue.
There was a problem hiding this comment.
Rate Limit Exceeded
@Hugo0 have exceeded the limit for the number of chat messages per hour. Please wait 1 minutes and 20 seconds before sending another message.
| try: | ||
| import openai | ||
|
|
||
| client = openai.OpenAI(api_key=openai_key) | ||
| lang_name = language_configs[lang_code].get("name", lang_code) |
There was a problem hiding this comment.
Unused variable lang_name (Ruff F841).
lang_name is assigned on line 990 but never referenced. It was likely intended for the prompt but isn't used there.
- lang_name = language_configs[lang_code].get("name", lang_code)🧰 Tools
🪛 Ruff (0.15.1)
[error] 990-990: Local variable lang_name is assigned to but never used
Remove assignment to unused variable lang_name
(F841)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@webapp/app.py` around lines 986 - 990, The variable lang_name is assigned
from language_configs[lang_code].get("name", lang_code) but never used, causing
an unused-variable warning; either remove that assignment if the human-readable
name isn't needed, or use lang_name where you currently embed lang_code in the
prompt/metadata so the user-facing text uses the friendly name—update the prompt
construction (the code that builds or formats the prompt near where client =
openai.OpenAI(...) is created) to refer to lang_name instead of lang_code, or
pass lang_name into the prompt/template builder that consumes language
information.
There was a problem hiding this comment.
Already fixed — the variable was removed when we refactored word_image() to use shared helpers.
There was a problem hiding this comment.
Rate Limit Exceeded
@Hugo0 have exceeded the limit for the number of chat messages per hour. Please wait 1 minutes and 13 seconds before sending another message.
…eanup - Add DATA_DIR env var for Render persistent disk (/data) with fallback to webapp/static - Add pending file lock to prevent duplicate DALL-E calls from concurrent requests - Limit image generation to top 10 languages (IMAGE_LANGUAGES) - Deduplicate pregenerate_images.py — reuse shared functions from app.py - Rework Today tab: word (linked to subpage), definition, image, share, timer - Move emoji grid to Stats tab with second share button - Add Wiktionary link on word subpages - Allow today's word on subpages (not just past words) - Remove unused rembg dependency - Remove console.log from production code
- Fix XSS: escape all Wiktionary API data before innerHTML interpolation - Fix TS strict: optional chaining on mock.calls[0] - Fix .env parser: strip surrounding quotes from values - Fix unbounded memory: clear _stats_seen_ips daily instead of growing forever - Fix concurrent stats writes: file locking around read-modify-write cycle - Fix urlretrieve timeout: use urlopen with 30s timeout for DALL-E downloads - Fix broad exception: catch specific OpenAI/network errors in image generation - Fix toggleDefinitions: reload definition when re-enabled after game completion - Fix unhandled promise: add .catch() to fetchDefinition promise chain - Fix JS injection: use tojson filter for lang_name_native in word.html script - Increase IMAGE_LANGUAGES from top 10 to top 30
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@webapp/app.py`:
- Around line 782-821: The function fetch_definition_cached currently writes an
empty dict to cache (json.dump(result or {}, f)) which causes
json.load(cache_path) to return {} instead of the documented None; change the
caching logic so that when no definition was found you write a JSON null (i.e.
dump None) instead of {} and/or normalize the loaded value to None (if
loaded_value == {}: loaded_value = None) so callers always receive None for a
cache miss; update the write site (reference: json.dump(result or {}, f) and
variables cache_path/cache_dir) and the read site (where json.load(f) is
returned) to enforce returning None for missing definitions.
In `@webapp/templates/game.html`:
- Around line 454-459: Close the unclosed <p> tag in the "Next word countdown"
block and remove the extraneous "this." from the Vue binding: ensure the element
containing the countdown inside the div is properly closed (add the missing
</p>) and change v-html="this.time_until_next_day" to
v-html="time_until_next_day" to match other bindings like emoji_board.
- Around line 399-421: The tab UI using the statsTab reactive variable lacks
ARIA roles and attributes; add accessibility by setting role="tablist" on the
container div and role="tab" on each button (the three buttons that set statsTab
to 'today', 'stats', and 'global'), bind aria-selected to true when statsTab
matches the button (e.g., :aria-selected="statsTab === 'today'"), and manage
focusability with tabindex (0 for the selected tab, -1 otherwise, e.g.,
:tabindex="statsTab === 'today' ? 0 : -1"); also consider adding unique
aria-controls ids per tab if corresponding tabpanels exist.
---
Duplicate comments:
In `@frontend/src/game.ts`:
- Around line 1265-1308: The build fails because the
SettingsChangeParams.setting union in analytics.ts does not include
'definitions' and 'word_art', causing calls to analytics.trackSettingsChange
(from functions toggleDefinitions and toggleWordArt) to be a type error; update
the SettingsChangeParams type declaration (the union for the setting field) in
frontend/src/analytics.ts to add 'definitions' and 'word_art' alongside
'dark_mode' | 'haptics' | 'sound' so the calls with setting: 'definitions' and
setting: 'word_art' compile.
- Around line 1310-1335: The fetchDefinition(...).then(renderDefinitionCard)
call inside loadDefinition() can produce unhandled promise rejections if
fetchDefinition rejects or renderDefinitionCard throws; update the block that
uses fetchDefinition(this.todays_word, langCode) so it attaches a .catch(...) to
handle errors (log to console or process logger, call a UI fallback like
hide/remove the loading state shown by showDefinitionLoading, and optionally
render an error state) and ensure any synchronous errors from
renderDefinitionCard are caught by resolving the then callback inside a
try/catch or by using .then(def => { try { renderDefinitionCard(...); } catch
(e) { /* handle */ } }). Refer to loadDefinition, fetchDefinition,
renderDefinitionCard, showDefinitionLoading and the definition-card container
when making the change.
- Around line 1265-1280: When re-enabling definitions or word art the UI never
repopulates after a finished game; update toggleDefinitions and toggleWordArt so
that after you set localStorage and track the change, if the feature is being
enabled (this.definitionsEnabled / this.wordArtEnabled is true) and the
definition/word-art content is not present (or the game is complete), call
this.loadDefinition() / this.loadWordArt() respectively to reload the card; add
the load calls immediately after analytics.trackSettingsChange in
toggleDefinitions (call this.loadDefinition()) and in toggleWordArt (call
this.loadWordArt()).
In `@scripts/pregenerate_images.py`:
- Line 111: The call to urlreq.urlretrieve inside generate_word_image is
blocking without a timeout; update generate_word_image (in webapp/app.py) to
download the image with a request that uses a timeout (e.g., use
urllib.request.urlopen or requests.get with timeout) and stream/save the
response to cache_path instead of calling urlreq.urlretrieve directly, and
ensure exceptions for timeouts/network errors are caught and handled/logged so
the cron job won't hang indefinitely.
In `@webapp/app.py`:
- Around line 979-999: Replace the blocking urlreq.urlretrieve call with a
urlreq.urlopen call that uses a timeout (e.g., urlreq.urlopen(image_url,
timeout=60)) and stream the response into the temporary file (writing
response.read() / iterating chunks into tmp_path) so the Flask thread won’t
hang; update the surrounding try/except/finally in the same block where tmp_path
is created to still unlink tmp_path on error. Also remove the now-dead except
ImportError fallback (the block that imports shutil and moves tmp_path to
cache_path) since pillow>=12.1.1 is guaranteed, keeping only the PIL.Image
open/save logic and the general Exception handler that returns f"error: {e}".
- Around line 828-853: _stats_seen_ips currently grows unbounded; change it to a
bounded per-day structure (e.g., a dict mapping day_idx -> set of ips) and evict
old days and/or enforce a max total entries (LRU or size cap) so keys like
"lang:day:ip" are removed when day advances or cap is exceeded; update all
usages of _stats_seen_ips to check per-day sets. For _load_word_stats and
_save_word_stats implement advisory file locking and atomic replace to avoid
lost updates across Gunicorn workers: acquire an exclusive lock (e.g.,
fcntl.flock) around the read-modify-write sequence in
_load_word_stats/_save_word_stats, write to a temp file then os.replace to
commit, and ensure mkdirs happen before locking; keep function names
_load_word_stats and _save_word_stats and handle/propagate errors instead of
silently passing.
…me card - Only generate images for day 1708 (Feb 21) onwards — older words serve cache only - Remove Wiktionary external links from in-game definition card (word subpage has them) - Hide definition card when no definition found (instead of showing Wiktionary link)
…English) Portuguese word links to pt.wiktionary.org instead of en.wiktionary.org, etc. Pass wikt_lang from route to template with lang code mapping.
- Remove "Today's Word" label from Today tab (word itself is enough) - Re-add arrow link icon on definition card, now points to word subpage instead of Wiktionary (word subpage has Wiktionary links)
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (3)
frontend/src/definitions.ts (1)
61-92: Add fetch timeouts to prevent indefinitely hung definition requests.Both
fetchFromEnWiktionaryandfetchFromNativeWiktionarycallfetch(url)with no timeout. A slow Wiktionary response will leave the loading skeleton in the stats modal visible forever — there's no fallback to the "link" source unless the request rejects.
AbortSignal.timeout(ms)is the idiomatic modern fix — it aborts the fetch and throws aTimeoutErrorwhich is already caught by the surroundingtry/catch:♻️ Proposed fix
- const response = await fetch(url); + const response = await fetch(url, { signal: AbortSignal.timeout(5000) });Apply the same change to the
fetch(url)call insidefetchFromNativeWiktionary(line 107).Also applies to: 106-139
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/src/definitions.ts` around lines 61 - 92, fetchFromEnWiktionary (and fetchFromNativeWiktionary) currently call fetch(url) with no timeout so a slow Wiktionary response can hang forever; update both functions to create an AbortSignal via AbortSignal.timeout(ms) (choose a reasonable timeout value) and pass it to fetch as the signal option, ensuring the fetch will abort on timeout and the existing try/catch will handle the thrown TimeoutError.frontend/src/game.ts (1)
1297-1312:toggleWordArtdoesn't reload the image when re-enabled after game completion.Unlike
toggleDefinitions(which callsloadDefinition()on re-enable),toggleWordArthas no equivalent logic. A user who disables word art and re-enables it post-game will see a blank image container.♻️ Proposed fix
analytics.trackSettingsChange({ setting: 'word_art', value: this.wordArtEnabled, }); + // Reload image if re-enabled after game completion + if (this.wordArtEnabled && this.game_over) { + this.loadDefinition(); + } }); },🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/src/game.ts` around lines 1297 - 1312, toggleWordArt currently persists the setting and tracks analytics but doesn’t reload the visual when re-enabled after a finished game; mirror the pattern used in toggleDefinitions by invoking the component’s image-loading routine when re-enabling word art (e.g., add a conditional after setting localStorage: if (this.wordArtEnabled && this.gameCompleted) call the existing loadWordArt() / renderWordArt() method used elsewhere to populate the image container), keeping the localStorage try/catch and analytics.trackSettingsChange as-is.webapp/app.py (1)
1188-1190: Broadexcept Exceptionswallows unexpected errors in stats submission (Ruff BLE001).
MemoryError,KeyboardInterrupt, and similar should propagate. Narrow the catch to expected failure modes:♻️ Proposed fix
- except Exception: - return "", 500 + except (ValueError, KeyError, OSError, IOError): + return "", 500🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@webapp/app.py` around lines 1188 - 1190, The catch-all "except Exception" that returns 500 after the stats submission swallows critical errors; replace it with a narrow catch for only the expected failure modes (for example ValueError, json.JSONDecodeError, and requests.exceptions.RequestException) in the try/except block that currently does "return '', 200" / "except Exception: return '', 500", and re-raise anything else (or omit it so it propagates). Ensure you import json and requests.exceptions.RequestException (or the HTTP client exception type you use) and update the except clause to "except (ValueError, json.JSONDecodeError, requests.exceptions.RequestException) as e:" and return the 500 there while letting KeyboardInterrupt, MemoryError, SystemExit, etc. propagate.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@frontend/src/game.ts`:
- Around line 1279-1282: The condition uses method references (this.gameWon,
this.gameLost) which are always truthy, so replace that check with the boolean
game-over flag: change the guard around loadDefinition() to use this.game_over
(and keep this.definitionsEnabled) so loadDefinition() only runs when
definitions are enabled and the game is actually over; locate the conditional
around loadDefinition() near the existing references to this.definitionsEnabled,
this.gameWon, this.gameLost and update it to check this.game_over instead.
In `@webapp/app.py`:
- Around line 1024-1028: The except ImportError block in webapp/app.py currently
moves the raw PNG at tmp_path to cache_path (which uses a .webp extension),
causing mismatched content-type; update the except ImportError handler for the
Image import so it either re-raises the ImportError (remove the fallback) or
moves/renames tmp_path to a .png cache filename instead of cache_path, and
ensure any downstream use of cache_path/send_from_directory will serve the
correct .png file; locate the ImportError handler referencing tmp_path and
cache_path and modify it accordingly.
- Around line 1015-1016: The code calls urlreq.Request(image_url) and
urlreq.urlopen(req, ...) without validating the URL scheme, which triggers Ruff
S310; update the code around image_url (the value from response.data[0].url) to
perform a one-line scheme check (e.g., parse the URL and ensure scheme ==
"https" or "http" as acceptable) before creating the Request and calling
urlreq.urlopen; if the scheme is invalid, handle it by logging/raising an error
or skipping processing so urlreq.Request and urlreq.urlopen are never invoked
with an unvalidated scheme.
---
Duplicate comments:
In `@frontend/src/game.ts`:
- Around line 1275-1278: The TypeScript union for SettingsChangeParams.setting
is missing 'definitions' and 'word_art', causing analytics.trackSettingsChange
calls in game.ts to fail; update the SettingsChangeParams type in
frontend/src/analytics.ts (the SettingsChangeParams interface/ type and any
related TrackSettingsChange payload type) to include 'definitions' and
'word_art' alongside 'dark_mode' | 'haptics' | 'sound' so that calls to
analytics.trackSettingsChange({ setting: 'definitions', ... }) and
analytics.trackSettingsChange({ setting: 'word_art', ... }) compile.
In `@webapp/app.py`:
- Around line 785-820: The cache currently writes an empty object when no
definition is found, causing json.load to return {} instead of the documented
None; in fetch_definition_cached change the cache write to persist the actual
result (allowing None to be written as JSON null) by replacing json.dump(result
or {}, f) with json.dump(result, f) so subsequent json.load(cache_path) returns
None when no definition was found (update in the block that creates cache_dir
and opens cache_path where variables result, cache_dir, cache_path are used).
---
Nitpick comments:
In `@frontend/src/definitions.ts`:
- Around line 61-92: fetchFromEnWiktionary (and fetchFromNativeWiktionary)
currently call fetch(url) with no timeout so a slow Wiktionary response can hang
forever; update both functions to create an AbortSignal via
AbortSignal.timeout(ms) (choose a reasonable timeout value) and pass it to fetch
as the signal option, ensuring the fetch will abort on timeout and the existing
try/catch will handle the thrown TimeoutError.
In `@frontend/src/game.ts`:
- Around line 1297-1312: toggleWordArt currently persists the setting and tracks
analytics but doesn’t reload the visual when re-enabled after a finished game;
mirror the pattern used in toggleDefinitions by invoking the component’s
image-loading routine when re-enabling word art (e.g., add a conditional after
setting localStorage: if (this.wordArtEnabled && this.gameCompleted) call the
existing loadWordArt() / renderWordArt() method used elsewhere to populate the
image container), keeping the localStorage try/catch and
analytics.trackSettingsChange as-is.
In `@webapp/app.py`:
- Around line 1188-1190: The catch-all "except Exception" that returns 500 after
the stats submission swallows critical errors; replace it with a narrow catch
for only the expected failure modes (for example ValueError,
json.JSONDecodeError, and requests.exceptions.RequestException) in the
try/except block that currently does "return '', 200" / "except Exception:
return '', 500", and re-raise anything else (or omit it so it propagates).
Ensure you import json and requests.exceptions.RequestException (or the HTTP
client exception type you use) and update the except clause to "except
(ValueError, json.JSONDecodeError, requests.exceptions.RequestException) as e:"
and return the 500 there while letting KeyboardInterrupt, MemoryError,
SystemExit, etc. propagate.
Many languages had "share" translated as the noun (portion/stock/quota) instead of the verb (to share/distribute). Fixed 20 mistranslations and added native translations for 11 languages that were using English default.
Remove dividers between related toggles to reduce clutter: - Haptic Feedback + Sound Effects grouped together - Show Definitions + Word Art grouped together
- Merge Haptic Feedback + Sound Effects → single "Sound & Haptics" toggle - Merge Show Definitions + Word Art → single "Word Info" toggle - Settings menu: 4 toggles → 2 toggles (plus dark mode and RTL) - New localStorage keys: feedbackEnabled, wordInfoEnabled - Updated both game page and homepage settings
Change DALL-E prompt from 3D render to flat vector illustration with green/white/gray palette matching the app's clean UI. Works better for abstract words (adjectives, verbs) and looks cohesive with the game.
- Add GET /<lang>/api/definition/<word> endpoint as single source of truth - Remove all Wiktionary client logic from frontend definitions.ts - Frontend now calls backend API instead of Wiktionary directly - Native Wiktionary parser handles 10+ language wiki formats (English, German, French, Spanish, Portuguese, Polish, Dutch, Italian, Swedish) - Fix short-definition bug for Dutch/Greek/Hebrew (lowered threshold, skip example sentences) - German title-case fallback (lowercase "depot" → tries "Depot") - Image generation uses English definitions for DALL-E prompt quality - New _fetch_english_definition() for DALL-E prompts, separate from user-facing native defs - Fix empty first definition in English Wiktionary (iterate all entries) - Switch image prompt to painterly style - Clear stale English-only definition caches for re-fetch with native definitions
- German: mbyte, polit (#57, #41) - Croatian: vruci, cerek, kadri, smoju, vazda, freud, david, romeo, vinko, jakob (#30-#55) - Hungarian: izzam (#53) - Esperanto: arnik (#102) - English: add "slave" to supplement (#49) Closes #30, #31, #32, #33, #34, #38, #39, #41, #48, #49, #50, #52, #53, #55, #57, #73, #102
Words added to blocklists must also be removed from the main word lists and daily_words files, otherwise test_daily_words_not_in_blocklist fails.
- Add URL scheme validation for wordPageUrl (prevents javascript: injection) - Pass actual attempt count on loss instead of hardcoded 0 - Move PIL import before temp file creation to prevent leaked tmp files - Fix critical AttributeError: use language_codes_5words dict instead of lang.word_list attribute (languages dict has no such attribute) - Fix Basque "share" translation (Zati→Partekatu) with trailing space - Fix Interlingue "share" translation (Compartir→Partir)
Blocklisted words should remain in _5words.txt so they're valid guesses. The blocklist system already prevents them from being daily words at runtime — removing them from the word list was redundant and prevented players from typing them as guesses.
Past daily words are now cached on first access so future word list changes can never alter historical results. Language._get_daily_word() delegates to the shared get_word_for_day() to eliminate logic duplication between the game and word subpages. Also adds a TODO to README about consolidating per-language data files.
Definition is now fetched client-side via the API endpoint instead of blocking the page render with a Wiktionary HTTP request. Cached definitions are still read from disk for OG meta tags (instant).
No more flash of "Look up on Wiktionary" for cached words. Definition is included in the HTML when the cache exists, JS fetch only runs when cache is empty (first visit).
Replace single sitemap with sitemap index + paginated child sitemaps. All ~111K word pages are now indexed (65 langs × 1709 days), split at 50K URLs per file. Recent words get higher priority (1.0→0.3).
- robots.txt with sitemap directive - llms.txt with project summary and all language links - JSON-LD WebApplication structured data on homepage - Canonical URLs on homepage, game pages, and word pages
- Add hreflang tags across all pages (65 languages) via shared partial - Add server-rendered language links on homepage (crawlable without JS) - Improve title tags: "Play Wordle in German — Free Daily Word Game" instead of "Wordle Deutsch/German" (targets search intent) - Improve homepage title/description with high-volume keywords - Add custom 404 page with language links (reduces dead-end bounces)
All 404 returns now go through Flask's error handler, showing the custom 404 page with language links instead of plain text.
|
@coderabbitai Please do a thorough review of the latest commits, focusing on:
|
🧩 Analysis chain🏁 Script executed: Length of output: 2324 🏁 Script executed: Length of output: 1055 🏁 Script executed: Length of output: 2527 🏁 Script executed: Length of output: 3644 🏁 Script executed: Length of output: 2193 🏁 Script executed: Length of output: 693 🏁 Script executed: Length of output: 997 🏁 Script executed: Length of output: 12280 🏁 Script executed: Length of output: 1125 🏁 Script executed: Length of output: 642 🏁 Script executed: Length of output: 920 🏁 Script executed: Length of output: 2361 🏁 Script executed: Length of output: 81 🏁 Script executed: Length of output: 470 🏁 Script executed: Length of output: 6498 🏁 Script executed: Length of output: 2105
1. 🔴 SEO Correctness — Critical Issue FoundCRITICAL: Missing hreflang on Word PagesIssue: The <!-- index.html and game.html both include it -->
{% include 'partials/_hreflang.html' %}
<!-- word.html is missing this include -->PROBLEM: Wrong hreflang URL PatternEven if you add the include, the global # Line 107 in app.py
"hreflang_url_pattern": "https://wordle.global/{lang}"This generates URLs like Recommendation: # In word_page route, override the pattern
`@app.route`("/<lang_code>/word/<int:day_idx>")
def word_page(lang_code, day_idx):
# ... existing code ...
return render_template(
"word.html",
# ... existing variables ...
hreflang_url_pattern=f"https://wordle.global/{{lang}}/word/{day_idx}",
)<!-- In word.html, add after canonical tag (line 32) -->
<link rel="canonical" href="https://wordle.global/{{ lang_code }}/word/{{ day_idx }}">
{% include 'partials/_hreflang.html' %}✅ What's Correct:
2.
|
There was a problem hiding this comment.
Actionable comments posted: 9
🧹 Nitpick comments (13)
webapp/data/languages/br/language_config.json (1)
15-15: LGTM — minor trailing-space inconsistency worth verifying.
"Rannañ"is the correct Breton translation. However, every sibling key in the"text"block still carries a trailing space ("Next word ","Copied to clipboard ", etc.), and the original"Share "did too. If the rendering layer concatenates these values directly without inserting its own space, dropping the trailing space here could produce a layout difference compared to other keys. Confirm this is intentional (ideally by stripping trailing spaces from all sibling entries in a follow-up cleanup).🧹 Optional: strip trailing spaces from all sibling entries for consistency
- "next_word": "Next word ", - "no_attempts": "You haven't tried any words yet! ", - "share": "Rannañ", - "notification-copied": "Copied to clipboard ", - "notification-partial-word": "Please enter a full word " + "next_word": "Next word", + "no_attempts": "You haven't tried any words yet!", + "share": "Rannañ", + "notification-copied": "Copied to clipboard", + "notification-partial-word": "Please enter a full word"Apply the equivalent cleanup across other language config files in the same PR for consistency.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@webapp/data/languages/br/language_config.json` at line 15, The "share" entry in the "text" block ("share": "Rannañ") no longer has the trailing space that all sibling keys currently include (e.g., "Next word ", "Copied to clipboard ", previous "Share "), so verify rendering expectations and make them consistent: either restore the trailing space on "share" to match existing siblings or better, remove trailing spaces from all entries in this "text" block (and optionally across other language_config.json files) and update the rendering layer to insert spaces as needed; locate the "share" key in the "text" object and apply the chosen consistent cleanup across sibling keys.webapp/data/languages/de/de_blocklist.txt (1)
42-43: Place new entries under appropriate section headers.
mbyteandpolitare appended after the last categorized block without a section comment, breaking the established structure.mbytefits under# English loanwords that feel out of placeandpolitunder# Political organization acronyms(or a new section if a broader "political terms" category is intended).♻️ Suggested reorganization
# English loanwords that feel out of place story stack excel sparc photo +mbyte # Offensive/inappropriate ... # Political organization acronyms nsdap +polit - -mbyte -polit🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@webapp/data/languages/de/de_blocklist.txt` around lines 42 - 43, Move the two orphaned entries into their correct section headers: place "mbyte" under the existing comment header "# English loanwords that feel out of place" and place "polit" under "# Political organization acronyms" (or create a new "# Political organization acronyms" section if it does not exist yet) so all blocklist entries remain grouped by the established section comments and file structure.webapp/data/languages/hu/hu_blocklist.txt (1)
1-2: Add a section comment explaining whyizzamis blocked.Unlike the German blocklist (which uses section headers like
# Offensive/inappropriate), this file has no rationale for the entry. A brief inline comment aids future maintainers in understanding whether this is offensive, a proper noun, etc.♻️ Suggested addition
# Hungarian blocklist + +# Offensive/inappropriate izzam🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@webapp/data/languages/hu/hu_blocklist.txt` around lines 1 - 2, Add a short inline comment above or beside the blocked token "izzam" in hu_blocklist.txt explaining the rationale (e.g., "offensive term", "personal name / PII", "spam-related", etc.) and follow the same style as the German file by using a section header like "# Offensive/inappropriate" if appropriate; update the entry so maintainers can see why "izzam" is blocked without guessing.webapp/templates/sitemap_main.xml (1)
1-13: Consider adding<lastmod>to URL entries for sitemap consistency
sitemap_main.xmlomits<lastmod>on all<url>entries whilesitemap_index.xmlsupplies<lastmod>{{ lastmod }}</lastmod>on every<sitemap>entry. Omitting it is valid per the Sitemap protocol, but including it for the language root pages helps search engines prioritise re-crawling more accurately.🔧 Optional addition
<url> <loc>{{ base_url }}</loc> + <lastmod>{{ lastmod }}</lastmod> <priority>1.0</priority> </url> {% for language in languages %} <url> <loc>{{ base_url }}/{{ language }}</loc> + <lastmod>{{ lastmod }}</lastmod> <priority>0.9</priority> </url> {% endfor %}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@webapp/templates/sitemap_main.xml` around lines 1 - 13, Add a <lastmod> element to each <url> entry in sitemap_main.xml by inserting <lastmod>{{ lastmod }}</lastmod> under the <loc> for both the base URL (using {{ base_url }}) and inside the loop for each language URL (the block iterating over {{ languages }}), so the template emits a lastmod timestamp for the root and language-specific URLs consistent with sitemap_index.xml.webapp/templates/word.html (1)
177-177:word.upper()andlang_codeare nottojson-encoded in the JS string.
lang_name_nativewas correctly fixed to use|tojson, but the other two Jinja2 values in the same string literal are still injected raw. Although Wordle words and language codes are currently safe, consistency protects against future edge cases.✨ Suggested change
- const text = 'Wordle ' + {{ lang_name_native|tojson }} + ' #{{ day_idx }} — {{ word.upper() }}\nhttps://wordle.global/{{ lang_code }}/word/{{ day_idx }}'; + const text = 'Wordle ' + {{ lang_name_native|tojson }} + ' #{{ day_idx }} \u2014 ' + {{ word.upper()|tojson }} + '\nhttps://wordle.global/' + {{ lang_code|tojson }} + '/word/{{ day_idx }}';🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@webapp/templates/word.html` at line 177, The JS string building the share text uses raw Jinja values for word.upper() and lang_code; update the template to JSON-encode those interpolations like lang_name_native is encoded so the JS receives safe JS strings—replace {{ word.upper() }} with {{ word.upper()|tojson }} and {{ lang_code }} with {{ lang_code|tojson }} in the const text assignment (the line that defines "const text" in the word.html template).frontend/src/definitions.ts (1)
50-100: Two optional improvements forrenderDefinitionCard.
def.url(Wiktionary URL) is never rendered. The PR objective calls for a "Wiktionary link" in the definition card, andWordDefinition.urlcarries this URL, but the function only surfaceswordPageUrl(the word subpage). Consider also renderingdef.urlas a secondary "Wiktionary ↗" link alongside the word-page icon.RTL text direction not set. For Hebrew, Arabic, and Persian definitions the rendered text will be displayed LTR. The
<p>element holding the definition could receivedir="rtl"when appropriate (e.g., passed via adirparameter or inferred fromlang). As per coding guidelines: "Consider right-to-left (RTL) language support when implementing UI features, particularly for Hebrew, Arabic, and Persian."🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/src/definitions.ts` around lines 50 - 100, renderDefinitionCard currently only renders the optional wordPageUrl and never surfaces def.url (the Wiktionary link) and doesn't set RTL direction for RTL languages; update renderDefinitionCard to (1) render def.url as a secondary link (label like "Wiktionary ↗") next to the existing linkHtml when def.url is present and safe (use the existing isSafeUrl and escapeHtml) and (2) set dir="rtl" on the <p> that contains the definition when a passed-in signal indicates RTL (e.g., add an optional dir or rtl boolean parameter to renderDefinitionCard and apply it when building container.innerHTML), referencing the function name renderDefinitionCard, the def.url property, wordPageUrl/linkHtml construction, and the container.innerHTML template so reviewers can locate the changes.webapp/templates/index.html (1)
335-341: Sound & Haptics toggle is missing a description line, unlike every other toggle.Dark Mode, and previously the separate Haptic/Sound toggles, all include a
text-smdescription span. The consolidated toggle omits it, leaving users without context about what the toggle controls.✨ Suggested addition
<div class="flex flex-col"> <span class="font-medium">{{ ui.sound_and_haptics or "Sound & Haptics" }}</span> + <span class="text-sm text-neutral-500 dark:text-neutral-400">{{ ui.sound_and_haptics_desc or "Sound effects and vibration" }}</span> </div>🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@webapp/templates/index.html` around lines 335 - 341, Add a missing description line for the "Sound & Haptics" toggle by inserting a small descriptive span under the label (matching other toggles' pattern) and use an existing UI key if available (e.g., ui.sound_and_haptics_description) or a sensible fallback like "Enable sound and vibration feedback". Update the block that renders the label and toggle (references: the span with class "font-medium" and the call to toggle_switch('feedbackEnabled', 'toggleFeedback()', ...)) so it includes a second span with class "text-sm" (and appropriate muted/text color classes) directly under the label span to match the other toggles' layout and styling.webapp/templates/game.html (2)
383-409: Tab panels lackrole="tabpanel"andaria-labelledby.The tab buttons correctly have
role="tab"and:aria-selected, but the corresponding content divs (lines 412, 449, 512) are missingrole="tabpanel"andaria-labelledby/idpairings. This is a minor a11y gap — screen readers won't associate panels with their tabs.♿ Suggested additions
- <div v-show="statsTab === 'today'"> + <div v-show="statsTab === 'today'" role="tabpanel" id="tabpanel-today" aria-labelledby="tab-today"> - <div v-show="statsTab === 'stats'"> + <div v-show="statsTab === 'stats'" role="tabpanel" id="tabpanel-stats" aria-labelledby="tab-stats"> - <div v-show="statsTab === 'global'"> + <div v-show="statsTab === 'global'" role="tabpanel" id="tabpanel-global" aria-labelledby="tab-global">And add matching
idattributes on the tab buttons (e.g.,id="tab-today").🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@webapp/templates/game.html` around lines 383 - 409, The tab panels are missing accessible associations: add id attributes to each tab button (e.g., id="tab-today", id="tab-stats", id="tab-global" on the buttons that set statsTab) and add role="tabpanel" plus aria-labelledby pointing to the matching button id on each corresponding content div that renders the today, stats and global panels (the containers tied to statsTab values). Ensure each panel also has :aria-hidden or similar visibility handling consistent with statsTab so only the active panel is exposed to assistive tech.
429-438: Duplicate share button markup across Today and Stats tabs.The share button block (class,
:class,v-on:click, inner spans) is copied verbatim. Consider extracting it into a Jinja partial or macro to keep the template DRY.Also applies to: 499-508
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@webapp/templates/game.html` around lines 429 - 438, The share button markup is duplicated in the template (same block around the Today and Stats tabs); extract it into a reusable Jinja partial or macro (e.g., create a macro render_share_button that uses shareButtonState, shareResults, language.config.text.copied and language.config.text.share) and replace both duplicated blocks with a call to that macro so the markup and bindings (v-on:click="shareResults", :class computed from shareButtonState, inner spans referencing language.config.text.*) are maintained in one place.webapp/app.py (3)
1256-1304: Sitemap pagination math is correct but uses inconsistent 404 pattern.Line 1269 returns
"Not found", 404as a plain string, while other routes useabort(404)which renders the 404 template. Consider usingabort(404)for consistency.♻️ Suggested fix
if offset >= total: - return "Not found", 404 + abort(404)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@webapp/app.py` around lines 1256 - 1304, In sitemap_words(), replace the plain return "Not found", 404 with abort(404) so the route uses the same 404 rendering flow as other handlers; ensure the Flask abort symbol is imported (from flask import abort) if not already present in the module so the change compiles cleanly.
1382-1383:generate_word_imagereturns internal error details in its string.Line 1383 returns
f"error: {e}"which may include stack trace or internal details (e.g., API key fragments in certain OpenAI errors). The caller at line 1487 only checksresult == "ok"and returns a generic 500, so the error string isn't exposed to the client. However, if logging is added to the caller later, this could leak. Consider logging the exception insidegenerate_word_imageand returning a simple sentinel.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@webapp/app.py` around lines 1382 - 1383, generate_word_image currently returns f"error: {e}" exposing internal exception details; instead catch the exception, log the full exception (using the existing logger) inside generate_word_image and return a simple sentinel (e.g. "error" or None) so callers that only compare to "ok" (the caller that checks result == "ok") don't receive internal details; update generate_word_image to use the logger to record the exception and change the return value to a non-detailed sentinel.
936-971: Silentexcept Exception: passon network errors hinders debugging.
_fetch_native_wiktionary(line 969),fetch_definition_cached(lines 990, 1021), and_fetch_english_definition(line 1056) all silently swallow exceptions. While definitions are optional, having zero visibility into fetch failures makes it hard to diagnose issues like Wiktionary API changes or DNS problems. Consider logging atdebuglevel at minimum.♻️ Example for _fetch_native_wiktionary
- except Exception: - pass + except Exception: + logging.debug("Native Wiktionary fetch failed for %s/%s", lang_code, try_word, exc_info=True)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@webapp/app.py` around lines 936 - 971, Replace the silent "except Exception: pass" blocks in _fetch_native_wiktionary, fetch_definition_cached, and _fetch_english_definition with an explicit exception handler that captures the exception (e) and logs it at debug level (including exc_info/stacktrace) using the module's existing logger (e.g., app.logger or logger) before continuing; specifically change each bare except to "except Exception as e: logger.debug('<contextual message> while fetching definition for %s (%s)', word_or_key, lang_code, exc_info=True)" so failures are visible without changing control flow.frontend/src/game.ts (1)
1185-1231: Inconsistent localStorage parsing betweenloadFeedbackPreferenceandloadWordInfoPreference.
feedbackEnableduses strictstored === 'true'(line 1189), whilewordInfoEnabledusesstored !== 'false'(line 1226). Both default totrue, but thefeedbackEnabledapproach means any unexpected stored value (e.g.,'1') would set it tofalse, losing the intended default. Consider using the same pattern for both —stored !== 'false'is safer for default-ON preferences.♻️ Suggested fix
loadFeedbackPreference(): void { try { const stored = localStorage.getItem('feedbackEnabled'); if (stored !== null) { - this.feedbackEnabled = stored === 'true'; + this.feedbackEnabled = stored !== 'false'; } else { this.feedbackEnabled = true; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/src/game.ts` around lines 1185 - 1231, The parsing for feedback preference in loadFeedbackPreference is stricter than loadWordInfoPreference; change the logic in loadFeedbackPreference (and any related write/read in toggleFeedback) to mirror the safe default-on pattern used in loadWordInfoPreference by treating any stored value other than the explicit string 'false' as enabled (i.e., set this.feedbackEnabled = stored !== 'false'), keep calling setHapticsEnabled/setSoundEnabled and saving via localStorage in toggleFeedback as-is, and ensure analytics.trackSettingsChange still reports the updated this.feedbackEnabled value.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@webapp/app.py`:
- Around line 471-492: The cache read in get_word_for_day currently returns an
empty string when the cached file contains only whitespace; change the logic to
treat a stripped-empty cache as invalid: after reading with f.read().strip(), if
the value is falsy (""), fall through to call _compute_word_for_day(lang_code,
day_idx) and proceed to cache that computed value (using the same os.makedirs +
open(cache_path, "w") flow when day_idx <= get_todays_idx()). Ensure you still
return a non-empty word and that a corrupt cache is overwritten with the
recomputed value.
- Around line 1520-1523: Replace the local duplicate mapping by using the
existing module-level _WIKT_LANG_MAP: remove the local wikt_lang_map definition
and change the lookup to use _WIKT_LANG_MAP.get(lang_code, lang_code) so
wikt_lang is derived from the shared map; ensure the code references
_WIKT_LANG_MAP and the variable names wikt_lang and lang_code remain unchanged.
- Around line 1464-1490: The current pending-file logic can block image
generation if the process dies before the finally removes pending_path; update
the block around pending_path creation/check to treat .pending files older than
a threshold (e.g., 5 minutes) as stale and remove them before returning 202.
Concretely: import time at the top, when seeing os.path.exists(pending_path) or
catching FileExistsError use os.path.getmtime(pending_path) and time.time() to
compare age and os.unlink stale files, then proceed to create the pending marker
and call generate_word_image/send_from_directory as before; ensure cleanup in
finally still runs.
In `@webapp/data/languages/en/en_5words_supplement.txt`:
- Line 10639: Remove the sensitive word "slave" from the answer pool in the
en_5words_supplement.txt word list (the entry "slave" shown in the diff), or
move it to a separate banned/filtered list used by the seeding/rotation logic so
it will not be selected as an answer; update any loader or validation that
builds the active answer set to exclude words from that banned list (ensure the
word "slave" is referenced there) and add a short test/assertion to confirm
banned words are not present in the generated daily answer set.
In `@webapp/data/languages/hyw/language_config.json`:
- Line 15: The "share" text entry in the hyw language JSON ("share") is missing
the trailing space used by other entries in the same "text" block; edit the
"share" value to include a trailing space ("Կիսուիլ ") to match the file's
spacing convention and flag the string for linguistic review—confirm with a
Western Armenian speaker whether the infinitive "Կիսուիլ" is the correct form
for a button label or if an imperative/noun (e.g., "Կիսե") should be used;
update the "share" value accordingly once the correct form is confirmed.
In `@webapp/data/languages/ltg/language_config.json`:
- Line 15: The "share" value in language_config.json is currently
"Daleit"—please verify this Latgalian translation against a reliable source
(native Latgalian speaker, authoritative dictionary, or Wiktionary entry) and
update the "share" key value if incorrect; reference the JSON key "share" and
confirm casing/diacritics match native orthography before committing the
corrected string.
In `@webapp/templates/index.html`:
- Around line 34-47: The JSON-LD block embeds the template variable description
raw, which can break the JSON if it contains quotes; change the description
interpolation in the WebApplication JSON-LD (the "{{ description }}" entry) to
use the tojson filter (i.e., render description | tojson) so it is properly
JSON-escaped within the <script type="application/ld+json"> block and matches
how languages are handled with "languages.keys() | list | tojson".
In `@webapp/templates/word.html`:
- Around line 5-34: The head section of the word.html template is missing the
hreflang alternate links include; add {% include 'partials/_hreflang.html' %}
inside the <head> (near the existing {% include 'partials/_base_head.html' %})
so language-specific rel="alternate" links render for the word pages, and update
the backend to set hreflang_url_pattern to "https://wordle.global/{lang}/word/{{
day_idx }}" so the partial can generate correct URLs for the Wordle page
(referencing the word.html template and the hreflang_url_pattern variable used
by the _hreflang.html partial).
- Around line 112-119: The totals are never updated because Jinja2 {% set %}
inside the {% for %} is block-scoped; change to use a namespace object (e.g., ns
= namespace(total_attempts=0, total_wins=0)) and update ns.total_attempts and
ns.total_wins inside the loop so the values persist after the loop, then compute
the average using ns.total_attempts and ns.total_wins. Also guard the max call
on word_stats.distribution.values() by checking if the distribution is non-empty
(e.g., only call values() | max when word_stats.distribution.values()
exists/length > 0) to avoid ValueError when the distribution is empty.
---
Duplicate comments:
In `@webapp/templates/word.html`:
- Around line 40-45: Replace hardcoded English strings in the word.html template
(e.g. "Play Wordle", "Community Stats", "Players", "Win Rate", "Avg Attempts",
"Share", "Play Today's Wordle") with lookups into the config.ui mapping with
sensible fallbacks, matching the pattern used in the game template; for example
change occurrences where the UI text is literal to use {{ config.ui.<key> or
'<fallback English>' }} (use consistent keys like play_wordle, community_stats,
players, win_rate, avg_attempts, share, play_today) so translations follow the
existing config.ui convention while preserving current English when keys are
missing.
- Around line 194-218: The inline fetch handler builds card.innerHTML using
unescaped data.part_of_speech and data.definition (and the templated {{ word
}}), creating an XSS risk; add a small escapeHtml helper in this script (or use
textContent/createElement) and apply it to data.part_of_speech and
data.definition (and the word output) before concatenating into card.innerHTML,
or instead build the DOM with createElement/appendChild to set textContent for
the parts; update the code that sets card.innerHTML in the fetch.then callback
(referencing card, fallback, data.part_of_speech, data.definition, and {{ word
}}) to use the escaped/safer values.
---
Nitpick comments:
In `@frontend/src/definitions.ts`:
- Around line 50-100: renderDefinitionCard currently only renders the optional
wordPageUrl and never surfaces def.url (the Wiktionary link) and doesn't set RTL
direction for RTL languages; update renderDefinitionCard to (1) render def.url
as a secondary link (label like "Wiktionary ↗") next to the existing linkHtml
when def.url is present and safe (use the existing isSafeUrl and escapeHtml) and
(2) set dir="rtl" on the <p> that contains the definition when a passed-in
signal indicates RTL (e.g., add an optional dir or rtl boolean parameter to
renderDefinitionCard and apply it when building container.innerHTML),
referencing the function name renderDefinitionCard, the def.url property,
wordPageUrl/linkHtml construction, and the container.innerHTML template so
reviewers can locate the changes.
In `@frontend/src/game.ts`:
- Around line 1185-1231: The parsing for feedback preference in
loadFeedbackPreference is stricter than loadWordInfoPreference; change the logic
in loadFeedbackPreference (and any related write/read in toggleFeedback) to
mirror the safe default-on pattern used in loadWordInfoPreference by treating
any stored value other than the explicit string 'false' as enabled (i.e., set
this.feedbackEnabled = stored !== 'false'), keep calling
setHapticsEnabled/setSoundEnabled and saving via localStorage in toggleFeedback
as-is, and ensure analytics.trackSettingsChange still reports the updated
this.feedbackEnabled value.
In `@webapp/app.py`:
- Around line 1256-1304: In sitemap_words(), replace the plain return "Not
found", 404 with abort(404) so the route uses the same 404 rendering flow as
other handlers; ensure the Flask abort symbol is imported (from flask import
abort) if not already present in the module so the change compiles cleanly.
- Around line 1382-1383: generate_word_image currently returns f"error: {e}"
exposing internal exception details; instead catch the exception, log the full
exception (using the existing logger) inside generate_word_image and return a
simple sentinel (e.g. "error" or None) so callers that only compare to "ok" (the
caller that checks result == "ok") don't receive internal details; update
generate_word_image to use the logger to record the exception and change the
return value to a non-detailed sentinel.
- Around line 936-971: Replace the silent "except Exception: pass" blocks in
_fetch_native_wiktionary, fetch_definition_cached, and _fetch_english_definition
with an explicit exception handler that captures the exception (e) and logs it
at debug level (including exc_info/stacktrace) using the module's existing
logger (e.g., app.logger or logger) before continuing; specifically change each
bare except to "except Exception as e: logger.debug('<contextual message> while
fetching definition for %s (%s)', word_or_key, lang_code, exc_info=True)" so
failures are visible without changing control flow.
In `@webapp/data/languages/br/language_config.json`:
- Line 15: The "share" entry in the "text" block ("share": "Rannañ") no longer
has the trailing space that all sibling keys currently include (e.g., "Next word
", "Copied to clipboard ", previous "Share "), so verify rendering expectations
and make them consistent: either restore the trailing space on "share" to match
existing siblings or better, remove trailing spaces from all entries in this
"text" block (and optionally across other language_config.json files) and update
the rendering layer to insert spaces as needed; locate the "share" key in the
"text" object and apply the chosen consistent cleanup across sibling keys.
In `@webapp/data/languages/de/de_blocklist.txt`:
- Around line 42-43: Move the two orphaned entries into their correct section
headers: place "mbyte" under the existing comment header "# English loanwords
that feel out of place" and place "polit" under "# Political organization
acronyms" (or create a new "# Political organization acronyms" section if it
does not exist yet) so all blocklist entries remain grouped by the established
section comments and file structure.
In `@webapp/data/languages/hu/hu_blocklist.txt`:
- Around line 1-2: Add a short inline comment above or beside the blocked token
"izzam" in hu_blocklist.txt explaining the rationale (e.g., "offensive term",
"personal name / PII", "spam-related", etc.) and follow the same style as the
German file by using a section header like "# Offensive/inappropriate" if
appropriate; update the entry so maintainers can see why "izzam" is blocked
without guessing.
In `@webapp/templates/game.html`:
- Around line 383-409: The tab panels are missing accessible associations: add
id attributes to each tab button (e.g., id="tab-today", id="tab-stats",
id="tab-global" on the buttons that set statsTab) and add role="tabpanel" plus
aria-labelledby pointing to the matching button id on each corresponding content
div that renders the today, stats and global panels (the containers tied to
statsTab values). Ensure each panel also has :aria-hidden or similar visibility
handling consistent with statsTab so only the active panel is exposed to
assistive tech.
- Around line 429-438: The share button markup is duplicated in the template
(same block around the Today and Stats tabs); extract it into a reusable Jinja
partial or macro (e.g., create a macro render_share_button that uses
shareButtonState, shareResults, language.config.text.copied and
language.config.text.share) and replace both duplicated blocks with a call to
that macro so the markup and bindings (v-on:click="shareResults", :class
computed from shareButtonState, inner spans referencing language.config.text.*)
are maintained in one place.
In `@webapp/templates/index.html`:
- Around line 335-341: Add a missing description line for the "Sound & Haptics"
toggle by inserting a small descriptive span under the label (matching other
toggles' pattern) and use an existing UI key if available (e.g.,
ui.sound_and_haptics_description) or a sensible fallback like "Enable sound and
vibration feedback". Update the block that renders the label and toggle
(references: the span with class "font-medium" and the call to
toggle_switch('feedbackEnabled', 'toggleFeedback()', ...)) so it includes a
second span with class "text-sm" (and appropriate muted/text color classes)
directly under the label span to match the other toggles' layout and styling.
In `@webapp/templates/sitemap_main.xml`:
- Around line 1-13: Add a <lastmod> element to each <url> entry in
sitemap_main.xml by inserting <lastmod>{{ lastmod }}</lastmod> under the <loc>
for both the base URL (using {{ base_url }}) and inside the loop for each
language URL (the block iterating over {{ languages }}), so the template emits a
lastmod timestamp for the root and language-specific URLs consistent with
sitemap_index.xml.
In `@webapp/templates/word.html`:
- Line 177: The JS string building the share text uses raw Jinja values for
word.upper() and lang_code; update the template to JSON-encode those
interpolations like lang_name_native is encoded so the JS receives safe JS
strings—replace {{ word.upper() }} with {{ word.upper()|tojson }} and {{
lang_code }} with {{ lang_code|tojson }} in the const text assignment (the line
that defines "const text" in the word.html template).
| def get_word_for_day(lang_code, day_idx): | ||
| """Get the daily word for a specific language and day index. | ||
|
|
||
| Once a word is computed for a past day, it's cached to disk so future | ||
| word list changes can never alter historical daily words. | ||
| """ | ||
| # Check cache first | ||
| cache_path = os.path.join(WORD_HISTORY_DIR, lang_code, f"{day_idx}.txt") | ||
| if os.path.exists(cache_path): | ||
| with open(cache_path, "r") as f: | ||
| return f.read().strip() | ||
|
|
||
| word = _compute_word_for_day(lang_code, day_idx) | ||
|
|
||
| # Cache past/current days (not future) | ||
| todays_idx = get_todays_idx() | ||
| if day_idx <= todays_idx: | ||
| os.makedirs(os.path.join(WORD_HISTORY_DIR, lang_code), exist_ok=True) | ||
| with open(cache_path, "w") as f: | ||
| f.write(word) | ||
|
|
||
| return word |
There was a problem hiding this comment.
get_word_for_day returns empty string on corrupt cache without fallback.
If the cached file exists but is empty or contains whitespace only, f.read().strip() returns "", and that empty string is used as the daily word. Add a validity check so corrupted cache falls through to recomputation.
🛡️ Proposed fix
cache_path = os.path.join(WORD_HISTORY_DIR, lang_code, f"{day_idx}.txt")
if os.path.exists(cache_path):
- with open(cache_path, "r") as f:
- return f.read().strip()
+ try:
+ with open(cache_path, "r") as f:
+ cached = f.read().strip()
+ if cached:
+ return cached
+ except IOError:
+ pass🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@webapp/app.py` around lines 471 - 492, The cache read in get_word_for_day
currently returns an empty string when the cached file contains only whitespace;
change the logic to treat a stripped-empty cache as invalid: after reading with
f.read().strip(), if the value is falsy (""), fall through to call
_compute_word_for_day(lang_code, day_idx) and proceed to cache that computed
value (using the same os.makedirs + open(cache_path, "w") flow when day_idx <=
get_todays_idx()). Ensure you still return a non-empty word and that a corrupt
cache is overwritten with the recomputed value.
| # Skip if another request is already generating this image | ||
| pending_path = cache_path + ".pending" | ||
| if os.path.exists(pending_path): | ||
| return "Image being generated", 202 | ||
|
|
||
| # Mark as pending to prevent duplicate DALL-E calls | ||
| os.makedirs(cache_dir, exist_ok=True) | ||
| try: | ||
| open(pending_path, "x").close() | ||
| except FileExistsError: | ||
| return "Image being generated", 202 | ||
|
|
||
| try: | ||
| # Use English definition for DALL-E prompt (DALL-E understands English best) | ||
| definition_hint = "" | ||
| en_def = _fetch_english_definition(word, lang_code) | ||
| if en_def: | ||
| definition_hint = f", which means {en_def}" | ||
|
|
||
| # Generate image via DALL-E | ||
| result = generate_word_image(word, definition_hint, openai_key, cache_dir, cache_path) | ||
| if result == "ok": | ||
| return send_from_directory(cache_dir, f"{word.lower()}.webp") | ||
| return "Image generation failed", 500 | ||
| finally: | ||
| if os.path.exists(pending_path): | ||
| os.unlink(pending_path) |
There was a problem hiding this comment.
Stale .pending file can permanently block image generation.
If the worker is killed (OOM, SIGKILL) after creating the pending file (line 1472) but before the finally block runs, the .pending file persists and all subsequent requests for that word return 202 indefinitely. Consider adding an age check (e.g., delete pending files older than 5 minutes).
🛡️ Proposed fix
pending_path = cache_path + ".pending"
- if os.path.exists(pending_path):
- return "Image being generated", 202
+ if os.path.exists(pending_path):
+ # Clean up stale pending files (e.g., from crashed workers)
+ try:
+ age = time.time() - os.path.getmtime(pending_path)
+ if age < 300: # 5 minutes
+ return "Image being generated", 202
+ os.unlink(pending_path)
+ except OSError:
+ pass(Requires import time at the top of the file.)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@webapp/app.py` around lines 1464 - 1490, The current pending-file logic can
block image generation if the process dies before the finally removes
pending_path; update the block around pending_path creation/check to treat
.pending files older than a threshold (e.g., 5 minutes) as stale and remove them
before returning 202. Concretely: import time at the top, when seeing
os.path.exists(pending_path) or catching FileExistsError use
os.path.getmtime(pending_path) and time.time() to compare age and os.unlink
stale files, then proceed to create the pending marker and call
generate_word_image/send_from_directory as before; ensure cleanup in finally
still runs.
|
|
||
| # Map language code to Wiktionary subdomain | ||
| wikt_lang_map = {"nb": "no", "nn": "no", "hyw": "hy", "ckb": "ku"} | ||
| wikt_lang = wikt_lang_map.get(lang_code, lang_code) |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
Duplicate wikt_lang_map definition — use the module-level _WIKT_LANG_MAP.
Line 1522 redefines the same mapping that already exists at line 780 as _WIKT_LANG_MAP.
♻️ Proposed fix
# Map language code to Wiktionary subdomain
- wikt_lang_map = {"nb": "no", "nn": "no", "hyw": "hy", "ckb": "ku"}
- wikt_lang = wikt_lang_map.get(lang_code, lang_code)
+ wikt_lang = _WIKT_LANG_MAP.get(lang_code, lang_code)📝 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.
| # Map language code to Wiktionary subdomain | |
| wikt_lang_map = {"nb": "no", "nn": "no", "hyw": "hy", "ckb": "ku"} | |
| wikt_lang = wikt_lang_map.get(lang_code, lang_code) | |
| # Map language code to Wiktionary subdomain | |
| wikt_lang = _WIKT_LANG_MAP.get(lang_code, lang_code) |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@webapp/app.py` around lines 1520 - 1523, Replace the local duplicate mapping
by using the existing module-level _WIKT_LANG_MAP: remove the local
wikt_lang_map definition and change the lookup to use
_WIKT_LANG_MAP.get(lang_code, lang_code) so wikt_lang is derived from the shared
map; ensure the code references _WIKT_LANG_MAP and the variable names wikt_lang
and lang_code remain unchanged.
| zygon | ||
| zymes | ||
| zymic | ||
| slave |
There was a problem hiding this comment.
Consider excluding "slave" from the answer pool.
The word is a valid dictionary entry and will be shuffled into the answer rotation via seed 42. However, "slave" carries deep historical trauma and has been explicitly removed from major word games (including NYT Wordle) precisely to avoid surfacing it as a daily answer. If the project has a content policy for sensitive words, this word likely falls under it.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@webapp/data/languages/en/en_5words_supplement.txt` at line 10639, Remove the
sensitive word "slave" from the answer pool in the en_5words_supplement.txt word
list (the entry "slave" shown in the diff), or move it to a separate
banned/filtered list used by the seeding/rotation logic so it will not be
selected as an answer; update any loader or validation that builds the active
answer set to exclude words from that banned list (ensure the word "slave" is
referenced there) and add a short test/assertion to confirm banned words are not
present in the generated daily answer set.
| "next_word": "Next word ", | ||
| "no_attempts": "You haven't tried any words yet! ", | ||
| "share": "Share ", | ||
| "share": "Կիսուիլ", |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
cat -n webapp/data/languages/hyw/language_config.jsonRepository: Hugo0/wordle
Length of output: 1794
🏁 Script executed:
# Check if other language files exist and their "share" entries
find webapp/data/languages -name "language_config.json" -type f | head -5 | xargs -I {} sh -c 'echo "=== {} ===" && grep -A 2 -B 2 "share" {}'Repository: Hugo0/wordle
Length of output: 1551
🏁 Script executed:
# Check English and more languages to see the pattern
grep -A 5 '"text"' webapp/data/languages/en/language_config.json | grep -A 6 '"text"'Repository: Hugo0/wordle
Length of output: 278
🏁 Script executed:
# Check Bulgarian more thoroughly - does it consistently omit trailing spaces?
grep -A 5 '"text"' webapp/data/languages/bg/language_config.jsonRepository: Hugo0/wordle
Length of output: 283
🏁 Script executed:
# Check more language files to understand the pattern - are there languages that consistently omit trailing spaces?
for lang in en ar az bg br ca cs cy da de el es fa fr gu he hi hu id it ja ka ko la pt ro ru sk sv ta te tr uk; do
file="webapp/data/languages/$lang/language_config.json"
if [ -f "$file" ]; then
share_line=$(grep '"share"' "$file")
echo "$lang: $share_line"
fi
doneRepository: Hugo0/wordle
Length of output: 909
🏁 Script executed:
# Let's check git history to see if this is a new file or modification
git log --oneline --all -- webapp/data/languages/hyw/language_config.json 2>/dev/null | head -5 || echo "No git history available"
# Also check if the file has other translations beyond the text block
grep -E '"(subheader|share)"' webapp/data/languages/hyw/language_config.jsonRepository: Hugo0/wordle
Length of output: 260
Trailing space inconsistent within this file's text block.
In the hyw language file, the "share" value differs from all other entries in the "text" block: "next_word", "no_attempts", "notification-copied", and "notification-partial-word" all retain trailing spaces, while "share": "Կիսուիլ" does not. This local inconsistency may cause UI spacing issues if the rendering code depends on that convention for this language.
Consider adding a trailing space: "share": "Կիսուիլ " to match the pattern in this file, or confirm that the omission is intentional.
Additionally, the infinitive form "Կիսուիլ" should be verified with a Western Armenian speaker—depending on UI context, an imperative form (e.g., "Կիսե") or noun form might be more natural as a button label.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@webapp/data/languages/hyw/language_config.json` at line 15, The "share" text
entry in the hyw language JSON ("share") is missing the trailing space used by
other entries in the same "text" block; edit the "share" value to include a
trailing space ("Կիսուիլ ") to match the file's spacing convention and flag the
string for linguistic review—confirm with a Western Armenian speaker whether the
infinitive "Կիսուիլ" is the correct form for a button label or if an
imperative/noun (e.g., "Կիսե") should be used; update the "share" value
accordingly once the correct form is confirmed.
| "next_word": "Next word ", | ||
| "no_attempts": "You haven't tried any words yet! ", | ||
| "share": "Share ", | ||
| "share": "Daleit", |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
find . -name "app.py" -type f | head -5Repository: Hugo0/wordle
Length of output: 71
🏁 Script executed:
rg -n "wiktionary|language_code" --type py -B 2 -A 2 | head -100Repository: Hugo0/wordle
Length of output: 6156
🏁 Script executed:
cat webapp/data/languages/ltg/language_config.jsonRepository: Hugo0/wordle
Length of output: 1533
🏁 Script executed:
rg -n "wiktionary|definition|look_up" --type py -B 3 -A 3Repository: Hugo0/wordle
Length of output: 15950
🏁 Script executed:
rg -n "language.*code" webapp/app.py | grep -i "wikt\|65\|mapping" -B 3 -A 3Repository: Hugo0/wordle
Length of output: 780
🏁 Script executed:
grep -n "ltg\|Wiktionary\|language_code_iso\|language_code_3" webapp/app.py -B 2 -A 2Repository: Hugo0/wordle
Length of output: 1880
🏁 Script executed:
rg -n "_WIKT_LANG_MAP" webapp/app.py -B 5 -A 50Repository: Hugo0/wordle
Length of output: 4868
Verify the Latgalian translation of "share".
The trailing-space cleanup is correct. The language code fields remain empty (pre-existing), which is acceptable—the code gracefully falls back to English Wiktionary for definitions if a native Latgalian Wiktionary is unavailable.
Confirm "Daleit" as the correct Latgalian word for "share" (plausible given the Latvian root dalīties, but worth verifying with a native speaker or dictionary).
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@webapp/data/languages/ltg/language_config.json` at line 15, The "share" value
in language_config.json is currently "Daleit"—please verify this Latgalian
translation against a reliable source (native Latgalian speaker, authoritative
dictionary, or Wiktionary entry) and update the "share" key value if incorrect;
reference the JSON key "share" and confirm casing/diacritics match native
orthography before committing the corrected string.
| <script type="application/ld+json"> | ||
| { | ||
| "@context": "https://schema.org", | ||
| "@type": "WebApplication", | ||
| "name": "Wordle Global", | ||
| "url": "https://wordle.global", | ||
| "description": "{{ description }}", | ||
| "applicationCategory": "GameApplication", | ||
| "operatingSystem": "Any", | ||
| "offers": { "@type": "Offer", "price": "0", "priceCurrency": "USD" }, | ||
| "inLanguage": {{ languages.keys() | list | tojson }}, | ||
| "browserRequirements": "Requires JavaScript" | ||
| } | ||
| </script> |
There was a problem hiding this comment.
JSON-LD description value is not JSON-escaped.
Line 40 embeds {{ description }} inside a manually quoted JSON string. If the description ever contains a " character (e.g., from a language name expansion), the JSON-LD block becomes invalid and search engines silently ignore it. In contrast, line 44 correctly uses | tojson. Apply the same pattern:
🔍 Suggested fix
- "description": "{{ description }}",
+ "description": {{ description | tojson }},📝 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.
| <script type="application/ld+json"> | |
| { | |
| "@context": "https://schema.org", | |
| "@type": "WebApplication", | |
| "name": "Wordle Global", | |
| "url": "https://wordle.global", | |
| "description": "{{ description }}", | |
| "applicationCategory": "GameApplication", | |
| "operatingSystem": "Any", | |
| "offers": { "@type": "Offer", "price": "0", "priceCurrency": "USD" }, | |
| "inLanguage": {{ languages.keys() | list | tojson }}, | |
| "browserRequirements": "Requires JavaScript" | |
| } | |
| </script> | |
| <script type="application/ld+json"> | |
| { | |
| "@context": "https://schema.org", | |
| "@type": "WebApplication", | |
| "name": "Wordle Global", | |
| "url": "https://wordle.global", | |
| "description": {{ description | tojson }}, | |
| "applicationCategory": "GameApplication", | |
| "operatingSystem": "Any", | |
| "offers": { "@type": "Offer", "price": "0", "priceCurrency": "USD" }, | |
| "inLanguage": {{ languages.keys() | list | tojson }}, | |
| "browserRequirements": "Requires JavaScript" | |
| } | |
| </script> |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@webapp/templates/index.html` around lines 34 - 47, The JSON-LD block embeds
the template variable description raw, which can break the JSON if it contains
quotes; change the description interpolation in the WebApplication JSON-LD (the
"{{ description }}" entry) to use the tojson filter (i.e., render description |
tojson) so it is properly JSON-escaped within the <script
type="application/ld+json"> block and matches how languages are handled with
"languages.keys() | list | tojson".
| <head> | ||
| {% include 'partials/_base_head.html' %} | ||
|
|
||
| {# Page-specific SEO meta tags #} | ||
| {% set title = "Wordle " ~ lang_name_native ~ " #" ~ day_idx ~ " — " ~ word.upper() %} | ||
| {% set def_text = definition.definition if definition and definition.definition else "" %} | ||
| {% set pos_text = definition.part_of_speech ~ ": " if definition and definition.part_of_speech else "" %} | ||
| {% if def_text %} | ||
| {% set description = "The Wordle word for " ~ lang_name ~ " #" ~ day_idx ~ " (" ~ word_date.strftime('%B %d, %Y') ~ ") was " ~ word.upper() ~ ". " ~ pos_text ~ def_text %} | ||
| {% else %} | ||
| {% set description = "The Wordle word for " ~ lang_name ~ " #" ~ day_idx ~ " (" ~ word_date.strftime('%B %d, %Y') ~ ") was " ~ word.upper() ~ ". Can you guess it in 6 tries?" %} | ||
| {% endif %} | ||
| <title>{{ title }}</title> | ||
| <meta name="description" content="{{ description[:200] }}"> | ||
| <meta property="og:title" content="{{ title }}"> | ||
| <meta property="og:url" content="https://wordle.global/{{ lang_code }}/word/{{ day_idx }}"> | ||
| <meta property="og:type" content="article"> | ||
| <meta property="og:description" content="{{ description[:200] }}"> | ||
| {# Word-specific AI art as social preview (generates on first crawl if not cached) #} | ||
| <meta property="og:image" content="https://wordle.global/{{ lang_code }}/api/word-image/{{ word }}?day_idx={{ day_idx }}"> | ||
| <meta property="twitter:image" content="https://wordle.global/{{ lang_code }}/api/word-image/{{ word }}?day_idx={{ day_idx }}"> | ||
| <meta name="twitter:card" content="summary_large_image"> | ||
| <meta property="twitter:domain" content="wordle.global"> | ||
| <meta property="twitter:url" content="https://wordle.global/{{ lang_code }}/word/{{ day_idx }}"> | ||
| <meta property="twitter:title" content="{{ title }}"> | ||
| <meta property="twitter:description" content="{{ description[:200] }}"> | ||
|
|
||
| <link rel="canonical" href="https://wordle.global/{{ lang_code }}/word/{{ day_idx }}"> | ||
| <link rel="preconnect" href="https://www.googletagmanager.com" crossorigin> | ||
| </head> |
There was a problem hiding this comment.
Missing {% include 'partials/_hreflang.html' %} in <head>.
Word pages are per-language and are the primary targets for the new sitemap/SEO work, yet they lack the hreflang alternate links. The backend also needs to set hreflang_url_pattern to https://wordle.global/{lang}/word/{{ day_idx }} for these pages.
🔍 Suggested addition
<link rel="canonical" href="https://wordle.global/{{ lang_code }}/word/{{ day_idx }}">
+ {% include 'partials/_hreflang.html' %}
<link rel="preconnect" href="https://www.googletagmanager.com" crossorigin>📝 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.
| <head> | |
| {% include 'partials/_base_head.html' %} | |
| {# Page-specific SEO meta tags #} | |
| {% set title = "Wordle " ~ lang_name_native ~ " #" ~ day_idx ~ " — " ~ word.upper() %} | |
| {% set def_text = definition.definition if definition and definition.definition else "" %} | |
| {% set pos_text = definition.part_of_speech ~ ": " if definition and definition.part_of_speech else "" %} | |
| {% if def_text %} | |
| {% set description = "The Wordle word for " ~ lang_name ~ " #" ~ day_idx ~ " (" ~ word_date.strftime('%B %d, %Y') ~ ") was " ~ word.upper() ~ ". " ~ pos_text ~ def_text %} | |
| {% else %} | |
| {% set description = "The Wordle word for " ~ lang_name ~ " #" ~ day_idx ~ " (" ~ word_date.strftime('%B %d, %Y') ~ ") was " ~ word.upper() ~ ". Can you guess it in 6 tries?" %} | |
| {% endif %} | |
| <title>{{ title }}</title> | |
| <meta name="description" content="{{ description[:200] }}"> | |
| <meta property="og:title" content="{{ title }}"> | |
| <meta property="og:url" content="https://wordle.global/{{ lang_code }}/word/{{ day_idx }}"> | |
| <meta property="og:type" content="article"> | |
| <meta property="og:description" content="{{ description[:200] }}"> | |
| {# Word-specific AI art as social preview (generates on first crawl if not cached) #} | |
| <meta property="og:image" content="https://wordle.global/{{ lang_code }}/api/word-image/{{ word }}?day_idx={{ day_idx }}"> | |
| <meta property="twitter:image" content="https://wordle.global/{{ lang_code }}/api/word-image/{{ word }}?day_idx={{ day_idx }}"> | |
| <meta name="twitter:card" content="summary_large_image"> | |
| <meta property="twitter:domain" content="wordle.global"> | |
| <meta property="twitter:url" content="https://wordle.global/{{ lang_code }}/word/{{ day_idx }}"> | |
| <meta property="twitter:title" content="{{ title }}"> | |
| <meta property="twitter:description" content="{{ description[:200] }}"> | |
| <link rel="canonical" href="https://wordle.global/{{ lang_code }}/word/{{ day_idx }}"> | |
| <link rel="preconnect" href="https://www.googletagmanager.com" crossorigin> | |
| </head> | |
| <head> | |
| {% include 'partials/_base_head.html' %} | |
| {# Page-specific SEO meta tags #} | |
| {% set title = "Wordle " ~ lang_name_native ~ " #" ~ day_idx ~ " — " ~ word.upper() %} | |
| {% set def_text = definition.definition if definition and definition.definition else "" %} | |
| {% set pos_text = definition.part_of_speech ~ ": " if definition and definition.part_of_speech else "" %} | |
| {% if def_text %} | |
| {% set description = "The Wordle word for " ~ lang_name ~ " #" ~ day_idx ~ " (" ~ word_date.strftime('%B %d, %Y') ~ ") was " ~ word.upper() ~ ". " ~ pos_text ~ def_text %} | |
| {% else %} | |
| {% set description = "The Wordle word for " ~ lang_name ~ " #" ~ day_idx ~ " (" ~ word_date.strftime('%B %d, %Y') ~ ") was " ~ word.upper() ~ ". Can you guess it in 6 tries?" %} | |
| {% endif %} | |
| <title>{{ title }}</title> | |
| <meta name="description" content="{{ description[:200] }}"> | |
| <meta property="og:title" content="{{ title }}"> | |
| <meta property="og:url" content="https://wordle.global/{{ lang_code }}/word/{{ day_idx }}"> | |
| <meta property="og:type" content="article"> | |
| <meta property="og:description" content="{{ description[:200] }}"> | |
| {# Word-specific AI art as social preview (generates on first crawl if not cached) #} | |
| <meta property="og:image" content="https://wordle.global/{{ lang_code }}/api/word-image/{{ word }}?day_idx={{ day_idx }}"> | |
| <meta property="twitter:image" content="https://wordle.global/{{ lang_code }}/api/word-image/{{ word }}?day_idx={{ day_idx }}"> | |
| <meta name="twitter:card" content="summary_large_image"> | |
| <meta property="twitter:domain" content="wordle.global"> | |
| <meta property="twitter:url" content="https://wordle.global/{{ lang_code }}/word/{{ day_idx }}"> | |
| <meta property="twitter:title" content="{{ title }}"> | |
| <meta property="twitter:description" content="{{ description[:200] }}"> | |
| <link rel="canonical" href="https://wordle.global/{{ lang_code }}/word/{{ day_idx }}"> | |
| {% include 'partials/_hreflang.html' %} | |
| <link rel="preconnect" href="https://www.googletagmanager.com" crossorigin> | |
| </head> |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@webapp/templates/word.html` around lines 5 - 34, The head section of the
word.html template is missing the hreflang alternate links include; add {%
include 'partials/_hreflang.html' %} inside the <head> (near the existing {%
include 'partials/_base_head.html' %}) so language-specific rel="alternate"
links render for the word pages, and update the backend to set
hreflang_url_pattern to "https://wordle.global/{lang}/word/{{ day_idx }}" so the
partial can generate correct URLs for the Wordle page (referencing the word.html
template and the hreflang_url_pattern variable used by the _hreflang.html
partial).
| {% set total_attempts = 0 %} | ||
| {% set total_wins = 0 %} | ||
| {% for k, v in word_stats.distribution.items() %} | ||
| {% set total_attempts = total_attempts + (k|int * v) %} | ||
| {% set total_wins = total_wins + v %} | ||
| {% endfor %} | ||
| <p class="text-lg font-bold">{{ (total_attempts / total_wins) | round(1) if total_wins > 0 else "—" }}</p> | ||
| <p class="text-[10px] text-neutral-500 dark:text-neutral-400">Avg Attempts</p> |
There was a problem hiding this comment.
{% set %} inside a {% for %} loop does not update the outer-scope variable — Avg Attempts always renders as "—".
In Jinja2, variable assignments inside a for block are scoped to that block. After the loop total_attempts and total_wins remain 0, so the if total_wins > 0 guard is always false. Use namespace():
🐛 Proposed fix using Jinja2 namespace
- {% set total_attempts = 0 %}
- {% set total_wins = 0 %}
- {% for k, v in word_stats.distribution.items() %}
- {% set total_attempts = total_attempts + (k|int * v) %}
- {% set total_wins = total_wins + v %}
- {% endfor %}
- <p class="text-lg font-bold">{{ (total_attempts / total_wins) | round(1) if total_wins > 0 else "—" }}</p>
+ {% set ns = namespace(total_attempts=0, total_wins=0) %}
+ {% for k, v in word_stats.distribution.items() %}
+ {% set ns.total_attempts = ns.total_attempts + (k|int * v) %}
+ {% set ns.total_wins = ns.total_wins + v %}
+ {% endfor %}
+ <p class="text-lg font-bold">{{ (ns.total_attempts / ns.total_wins) | round(1) if ns.total_wins > 0 else "—" }}</p>Additionally, line 124 calls word_stats.distribution.values() | max which raises ValueError on an empty distribution (e.g., all players lost). Add a guard:
- {% set max_count = word_stats.distribution.values() | max %}
+ {% set max_count = (word_stats.distribution.values() | max) if word_stats.distribution else 0 %}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@webapp/templates/word.html` around lines 112 - 119, The totals are never
updated because Jinja2 {% set %} inside the {% for %} is block-scoped; change to
use a namespace object (e.g., ns = namespace(total_attempts=0, total_wins=0))
and update ns.total_attempts and ns.total_wins inside the loop so the values
persist after the loop, then compute the average using ns.total_attempts and
ns.total_wins. Also guard the max call on word_stats.distribution.values() by
checking if the distribution is non-empty (e.g., only call values() | max when
word_stats.distribution.values() exists/length > 0) to avoid ValueError when the
distribution is empty.
- Add hreflang to word.html with correct URL pattern per word page - Harden word history cache: try/except on read, atomic writes via os.replace - Stop leaking exception details in image generation (log + generic error) - Replace raw HTML /stats endpoint with proper stats page showing language word counts, community play data, and word count bar chart - Stats data cached in memory (5 min TTL) to avoid scanning disk on each hit - Remove old status_list.txt dependency - Add /stats to sitemap
|
Thanks for the thorough review @coderabbitai! All 4 findings addressed in 1b704db: 1. 🔴 Missing hreflang on word pages — Fixed. Added 2. 3. 4. ✅ Async definitions + sitemap — Confirmed correct, no changes needed. Also in this commit: replaced the old raw-HTML |
Use max-w-lg container, same card styles, bar chart, and label sizes as word.html. Replace table with compact list view.
|
Tip For best results, initiate chat on the files or code changes. 🧩 Analysis chain🏁 Script executed: Length of output: 2362 🏁 Script executed: Length of output: 814
✅ Verified Fixes1. 🔴 hreflang on word pages — Confirmed!
2. # Graceful read with try/except
try:
with open(cache_path, "r") as f:
cached = f.read().strip()
if cached:
return cached
except OSError:
pass # Fall through to recompute
# Atomic write via temp file
tmp_path = cache_path + ".tmp"
with open(tmp_path, "w") as f:
f.write(word)
os.replace(tmp_path, cache_path) # Atomic on POSIX ✓3. except (openai.OpenAIError, urlreq.URLError, IOError, OSError) as e:
logging.error(f"Image generation failed for {word}: {e}")
return "error" # Generic, no details leaked4. 🆕 Stats page — Nice addition!
All critical SEO issues and security concerns are now resolved. The implementation quality is high — atomic writes, specific exception handling, and proper error recovery. Great job! 🚀 If you found this review helpful, would you consider giving us a shout-out on X? Thank you for using CodeRabbit! |
- Reuse fetch_definition_cached for DALL-E hints (deduplicate logic) - Add 'definitions' to SettingsChangeParams type union - Stats bar chart: fixed-width monospace lang codes (w-10 font-mono), more spacing (space-y-1.5), taller bars (h-3.5) - .env quote stripping and unused lang_name already fixed in prior commits - pregenerate_images.py urlretrieve already fixed (uses app.generate_word_image)
- Add 'definitions' to SettingsChangeParams type union
- Reuse fetch_definition_cached for DALL-E image hints (dedup)
- Stats bar chart: monospace fixed-width lang codes, more spacing
- Add "Since {date}" disclaimer on community stats section
|
@coderabbitai Please review the new |
|
Tip For best results, initiate chat on the files or code changes. 🧩 Analysis chain🏁 Script executed: Length of output: 9245 🏁 Script executed: Length of output: 3490 🏁 Script executed: Length of output: 1771
✅ Previous Fixes ConfirmedAll four critical/high-priority issues from the last review are properly fixed:
📊 Stats Page Implementation Review✅ Strong Points1. Clean Architecture # Line 1259-1263: Simple route with cached data
`@app.route`("/stats")
def stats():
data = _build_stats_data()
return render_template("stats.html", **data)
2. Effective Caching # Lines 1175-1185: In-memory cache with TTL
_stats_cache = {"data": None, "ts": 0}
_STATS_CACHE_TTL = 300 # 5 minutes
if _stats_cache["data"] and now - _stats_cache["ts"] < _STATS_CACHE_TTL:
return _stats_cache["data"]
3. SEO-Friendly Template
4. Security — No XSS Risks
|
- Catch json.JSONDecodeError/OSError specifically, log warnings - Skip non-numeric filenames safely (ValueError guard on int parse)
- Fix XSS: escape all Wiktionary API data before innerHTML interpolation - Fix TS strict: optional chaining on mock.calls[0] - Fix .env parser: strip surrounding quotes from values - Fix unbounded memory: clear _stats_seen_ips daily instead of growing forever - Fix concurrent stats writes: file locking around read-modify-write cycle - Fix urlretrieve timeout: use urlopen with 30s timeout for DALL-E downloads - Fix broad exception: catch specific OpenAI/network errors in image generation - Fix toggleDefinitions: reload definition when re-enabled after game completion - Fix unhandled promise: add .catch() to fetchDefinition promise chain - Fix JS injection: use tojson filter for lang_name_native in word.html script - Increase IMAGE_LANGUAGES from top 10 to top 30
feat: Word definitions after game completion
Summary
Closes #99. Shows the daily word's definition in the stats modal after game completion (win or loss).
OPENAI_API_KEYenv var — generates watercolor illustrations, cached to diskHow it works
loadDefinition()fires asyncAI Images (opt-in)
OPENAI_API_KEYenv var to enable/<lang>/api/word-image/<word>webapp/static/word-images/(gitignored)Test plan
pnpm test— 61 tests pass (7 new definition tests)uv run pytest tests/— 2001 passed, 290 skipped, 4 xfailedpnpm build— builds successfullySummary by CodeRabbit
New Features
Tests
Chores
Bug Fixes