feat: RTL fix, diacritic popup, accessibility, PostHog proxy#180
Conversation
docs/ is gitignored — these files were force-added before the rule. Internal analytics and architecture docs should stay local only.
…ty, PostHog proxy ## Bug Fixes - Fix Hebrew/Arabic/Persian/Kurdish/Urdu RTL typing (#178) — CSS `direction: rtl` on tile grid instead of array reversal - Fix cross-language game state bleed — `resetGameState()` before `loadFromLocalStorage()`, plus `definePageMeta({ key })` for full remount - Fix Esperanto diacritic input (#177) — iOS-style long-press popup for accented characters on all languages with diacritic maps ## Accessibility (WCAG 2.1 AA — zero visual impact for sighted users) - Screen reader: tile states announced ("A, correct position"), guess results via aria-live region - Screen reader: toast notifications use `role="alert"` with `aria-live="assertive"` - Screen reader: header buttons labeled ("How to play", "Statistics", "Settings") - Screen reader: keyboard key states announced ("c, correct" / "e, absent") - Screen reader: Enter/Backspace keys properly labeled (not Unicode symbols) - Keyboard: skip-to-content link, focus-visible rings on all interactive elements - Modals: `role="dialog"`, `aria-modal="true"`, `aria-labelledby` support - Reduced motion: all animations disabled when OS prefers-reduced-motion is set (CSS + JS) - Semantic HTML: game board uses role="grid"/role="row"/role="gridcell" - New /accessibility page with WCAG statement - WCAG badge in README, GitHub repo topics (a11y, accessibility, wcag, etc.) ## Analytics - PostHog: migrated from manual init to @posthog/nuxt module with /t proxy route (defeats ad blockers) - PostHog: `capture_pageview: 'history_change'` for automatic SPA pageview tracking - invalid_word events now track the actual word (capped at 10 chars) - Error tracking delegated to @posthog/nuxt module (automatic $exception capture) ## Code Quality - Extracted `isStandalone()` to shared `utils/storage.ts` (was duplicated in 2 files) - Extracted `getOrCreateId()` usage (was duplicated in analytics + game store) - Removed dead RTL parameter from `animateRevealRow()` signature - Cleaned up animation callbacks (visualIdx === dataIdx after RTL CSS change) - ToggleSwitch: added `role="switch"`, `aria-label` prop ## Tests - 4 E2E tests for diacritic popup (long-press, slide-select, quick-tap, no-popup) - 1 E2E test for cross-language state isolation - All 150 vitest + 25/26 E2E tests passing
|
Warning Rate limit exceeded
⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (68)
📝 WalkthroughWalkthroughAdds accessibility attributes and reduced-motion support, implements an iOS-style diacritic popup with touch/mouse/haptic handling and tests, migrates PostHog to Changes
Sequence Diagram(s)sequenceDiagram
actor User
participant Key as KeyboardKey
participant Popup as DiacriticPopup
participant Keyboard as GameKeyboard
participant Store as Game Store
User->>Key: touchstart / mousedown (long-press)
Key->>Key: start long-press timer, track pointer/movement
Key->>Popup: render popup (Teleport) with base + variants
Popup->>User: show variant options
User->>Popup: drag / select variant
Popup->>Key: selection event (chosen variant)
Key->>Keyboard: emit press with selected variant
Keyboard->>Store: update input / game state
Note over Key,Popup: quick tap emits base char without showing popup
sequenceDiagram
participant App as Application
participant Storage as utils/storage
participant NuxtMod as `@posthog/nuxt`
participant Proxy as /t/* proxy
participant Up as eu.i.posthog.com
App->>Storage: getOrCreateId('client_id')
NuxtMod->>App: module initialized (posthogConfig)
App->>NuxtMod: capture/identify (guarded)
App->>Proxy: request to /t/* (forwarded)
Proxy->>Up: proxied request with x-forwarded-for
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Poem
✨ Finishing Touches🧪 Generate unit tests (beta)
|
There was a problem hiding this comment.
Actionable comments posted: 13
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
composables/useAnalytics.ts (1)
594-599:⚠️ Potential issue | 🟠 MajorDon't ship raw invalid guesses to PostHog.
This is still free-form user input. A 10-character cap doesn't remove the privacy/compliance risk, and names or other arbitrary text can still be captured and retained by a third party. Prefer derived fields like length/script/diacritic presence, or keep literal values in a first-party-only diagnostic path instead.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@composables/useAnalytics.ts` around lines 594 - 599, The trackInvalidWord implementation currently sends raw user input (params.word) to PostHog; remove the literal word from the payload and instead send derived features (e.g., word_length, script/category, has_diacritics boolean, attempt_number, language) from within trackInvalidWord so no free‑form text is transmitted to the third party, and if you still need the raw value for first‑party diagnostics store it in a separate internal-only path (not via track). Update the call in trackInvalidWord (the track(...) invocation) and the InvalidWordParams usage accordingly to compute and send only those derived fields.
🧹 Nitpick comments (4)
components/game/GameKeyboard.vue (1)
2-2: Localize the keyboard accessible name instead of hardcoding English.
aria-label="Keyboard"is always English. In non-English locales, screen reader output should use localized text.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@components/game/GameKeyboard.vue` at line 2, The hardcoded aria-label "Keyboard" in the GameKeyboard.vue root div should be localized; update the element to bind aria-label to your i18n translation function (e.g., use $t or the composition API t) instead of the static string so screen readers receive the locale-specific label—locate the div with role="group" aria-label="Keyboard" and replace the literal with a translated key like t('keyboard.label') (or inject/prop the localized string) and ensure the translation key exists in your locale files.pages/[lang]/words.vue (1)
311-312: Consider adding fallback values for localized strings.The
langStore.config?.ui?.playsandlangStore.config?.ui?.winbindings will render as empty if the config isn't loaded or the key is missing. Consider providing fallback values for resilience:{{ w.stats.total }} {{ langStore.config?.ui?.plays || 'plays' }}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@pages/`[lang]/words.vue around lines 311 - 312, The localized UI labels may be undefined causing empty output; update the template bindings around the plays and win labels to use safe fallbacks (e.g., nullish coalescing) so that langStore.config?.ui?.plays and langStore.config?.ui?.win default to a sensible string like "plays" and "wins" when missing; change the expressions where w.stats.total and winRate(w.stats) are rendered to append the fallback labels instead of relying on the optional config only, ensuring the UI shows readable text even if langStore.config is not loaded.pages/[lang]/word/[id].vue (1)
536-544: Consider localizing remaining hardcoded strings for consistency.While several UI strings now use
langStore.config?.ui?., some remain hardcoded in English (e.g., "Win Rate", "Avg Attempts"). For a consistent multilingual experience, these could be migrated to the language config as well.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@pages/`[lang]/word/[id].vue around lines 536 - 544, The template contains hardcoded UI strings "Win Rate" and "Avg Attempts" near the avgAttempts binding; replace them with keys from the language config (e.g., use langStore.config?.ui?.winRate and langStore.config?.ui?.avgAttempts or similar names) and update any corresponding config/interface so these keys exist; ensure you provide safe fallbacks (default English strings) when langStore.config is undefined and update any tests/components that expect these strings.components/game/GameHeader.vue (1)
5-5: Consider using localized strings foraria-labelattributes.The
aria-labelvalues are hardcoded in English ("How to play", "Statistics", "Settings"), but other components in this PR uselangStore.config?.ui?.statisticsandlangStore.config?.ui?.settingsfor localized text. For consistency and full i18n support, consider sourcing these labels from the language store.♻️ Proposed refactor for localized aria-labels
+<script setup lang="ts"> +const langStore = useLanguageStore(); defineProps<{ langCode: string }>(); defineEmits<{ help: []; stats: []; settings: [] }>(); </script>Then in the template:
-<button class="z-40 text-neutral-500 p-2" aria-label="How to play" `@click`="$emit('help')"> +<button class="z-40 text-neutral-500 p-2" :aria-label="langStore.config?.ui?.how_to_play || 'How to play'" `@click`="$emit('help')">-<button class="m-0 sm:my-1 p-2" aria-label="Statistics" `@click`="$emit('stats')"> +<button class="m-0 sm:my-1 p-2" :aria-label="langStore.config?.ui?.statistics || 'Statistics'" `@click`="$emit('stats')">-<button class="m-0 sm:my-1 p-2" aria-label="Settings" `@click`="$emit('settings')"> +<button class="m-0 sm:my-1 p-2" :aria-label="langStore.config?.ui?.settings || 'Settings'" `@click`="$emit('settings')">Also applies to: 38-38, 59-59
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@components/game/GameHeader.vue` at line 5, Replace hardcoded English aria-label values in GameHeader.vue with localized strings from langStore (e.g., use langStore.config?.ui?.howToPlay for the help button, and langStore.config?.ui?.statistics and langStore.config?.ui?.settings for the other buttons referenced at lines 38 and 59); update the template attribute bindings to use these langStore properties with sensible fallbacks (e.g., a short English string) so aria-label reads from the language config while preserving the existing click handlers/emitters like the help button's `@click`="$emit('help')".
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@components/game/KeyboardKey.vue`:
- Line 7: The ARIA label strings are hard-coded in English; update the component
to use the app's i18n translations instead. Replace direct literals in the
:aria-label binding (the char checks for '⇨' and '⌫' and the keyAriaLabel usage)
with calls to the translation function (e.g., $t or the composition API t) such
as $t('keyboard.enter'), $t('keyboard.backspace') and for status labels replace
'correct'/'present'/'absent' with
$t('keyboard.status.correct')/$t('keyboard.status.present')/$t('keyboard.status.absent');
for the “hold for …” fragment use a translation with interpolation like
$t('keyboard.holdFor', { key: keyLabel }). Ensure any computed or method that
builds keyAriaLabel (and similar logic referenced in this file) uses these
translation keys so all ARIA output is localized.
- Around line 9-12: The touchend handler currently only emits when the variant
popup opened, so quick taps on diacritic-capable keys produce no output; update
handleTouchEnd to call emit('press', props.char) in the else path when the popup
never opened (i.e., when the popup open flag/state is false or its show timeout
never fired) so quick taps will emit the base character; reference
handleTouchEnd, the popup open state/timeout variable, and props.char when
making this change.
In `@components/game/NotificationToast.vue`:
- Around line 4-6: The toast component NotificationToast.vue currently uses
role="alert" and aria-live="assertive" which can preempt other announcements;
update the NotificationToast element to use a less intrusive live region (e.g.,
role="status" and aria-live="polite") or remove the assertive announcement so it
won't interrupt the board’s polite live-region updates; keep aria-atomic if you
need whole-message updates but ensure NotificationToast's live-region settings
are changed from "alert/assertive" to "status/polite" (or equivalent) to avoid
interrupting screen reader output.
In `@components/game/Tile.vue`:
- Around line 18-28: The computed ariaLabel currently builds English state text
inline in ariaLabel; replace those hardcoded strings with localized UI strings
from your i18n or language store (e.g., use the translation function or reactive
store values inside the computed) so the state fragments (Empty, ", correct
position", ", wrong position", ", not in word") come from translations. Update
the ariaLabel computed to pull localized strings (e.g., t('tile.empty'),
t('tile.correctPosition'), t('tile.wrongPosition'), t('tile.notInWord')) and
preserve the existing tileClass checks (props.tileClass.includes('correct'),
'semicorrect', 'incorrect') and letter handling, ensuring a safe fallback to
English or a default translation if translation keys are missing.
In `@components/shared/BaseModal.vue`:
- Line 6: Make the dialog accessible name mandatory by updating BaseModal's
props so that either labelId or ariaLabel must be provided: add a props-level
validator (or mark labelId as required) that enforces (props.labelId ||
props.ariaLabel) is truthy and surface a clear dev warning/error when neither is
supplied; keep the template binding :aria-labelledby="labelId" but allow the
component to set aria-label when ariaLabel is provided as the fallback.
Reference the BaseModal component and the labelId prop (and ariaLabel prop) when
making this change so every modal instance fails fast if no accessible name is
supplied.
In `@components/shared/ToggleSwitch.vue`:
- Line 8: The component currently binds :aria-label="label" allowing an
undefined accessible name; update the ToggleSwitch Vue component so the label
prop cannot be undefined (make the prop required: label: { type: String,
required: true } or provide a deterministic fallback string) and ensure the
template bindings that use label (the :aria-label on the switch and the other
instances around lines 18-21) use this required/fallback value so the rendered
switch always has an accessible name for screen readers.
In `@pages/accessibility.vue`:
- Around line 8-10: The meta description currently asserts full compliance;
update the description property in the component (the description string inside
the export default in pages/accessibility.vue) to avoid claiming WCAG 2.1 AA
compliance — rephrase to indicate a goal or ongoing effort (e.g., "aims for" or
"working towards WCAG 2.1 AA compliance") and keep the rest of the summary about
languages, screen reader support, keyboard navigation, and reduced motion
intact.
In `@README.md`:
- Around line 4-5: The README has inconsistent language counts: the badge string
"languages-79" and the textual mention "78" disagree; update the README so both
the badge token (e.g., the alt/label in the shields.io link) and the in-page
text mention use the same, correct number (choose the actual current language
count), and search for any other occurrences of "languages-79" or "78" in the
README to make them consistent; after editing, preview the README to confirm the
badge and text now match.
In `@server/routes/t/`[...path].ts:
- Around line 13-17: The current check relies only on
getHeader('content-length') so requests missing or with malformed content-length
bypass the limit; update the logic around contentLength, parseInt, and
setResponseStatus to enforce a hard body-size cap by actively reading the
request body stream and aborting when the accumulated bytes exceed the 1_000_000
limit. Specifically: treat absent or non-numeric contentLength as unknown (do
not assume small), start consuming the incoming stream from event (e.g., the raw
request stream used by your framework), count bytes as they arrive and call
setResponseStatus(event, 413) and stop processing if the count exceeds
1_000_000; retain the quick header-based check as an early exit but always
enforce the streaming byte-limit fallback for robustness.
- Around line 23-29: The code is currently taking the inbound 'x-forwarded-for'
header via getHeader(event, 'x-forwarded-for') and forwarding it directly,
allowing clients to spoof IPs; change the logic in the t/[...path].ts route so
that clientIp is derived only from the server-resolved address (use
getRequestIP(event) exclusively) and do not forward the raw incoming header
value. Update the proxyRequest headers construction (where 'x-forwarded-for' is
set) to send only the server-resolved client IP (from getRequestIP) or omit the
header if that value is absent; reference getHeader, getRequestIP, proxyRequest,
and the 'x-forwarded-for' usage to locate and update the code.
In `@server/utils/definitions.ts`:
- Around line 287-292: The cache-check logic currently always falls through to
the LLM when a record has loaded.source === 'kaikki-en' and no
loaded.definition_native, causing repeated expensive retries; modify the logic
that guards the LLM call to consult cooldown metadata on the cached object
(e.g., add fields like loaded.nextNativeEnrichRetryAt or
loaded.nativeEnrichFailedCount) and only call the LLM when the cooldown has
expired or retry count is below a threshold; when the LLM attempt fails, write
back the cache with updated cooldown/next retry timestamp and incremented failed
count so subsequent requests skip LLM until the cooldown expires; apply the same
change to the analogous branch referenced at lines 307-314 so both
native-enrichment paths respect the retry cooldown metadata.
In `@stores/game.ts`:
- Around line 535-550: The announcement currently hard-codes English tokens
("Row", "correct", "present", "absent") when building srAnnouncement in the
block using tiles, tileColors and revealingRow; replace those literals with
lookups into the app's localization/language config (e.g., use the existing
i18n/locale helper you use elsewhere) so you construct the parts using
translated labels for the row prefix and for each tile state, then set
srAnnouncement.value to a localized template (for example a translated "Row {n}:
{items}" pattern) so screen readers speak the localized strings instead of
English.
- Around line 910-943: resetGameState() currently clears board arrays but not
transient UI flags or ongoing async/timers; update resetGameState() (and call it
early in loadFromLocalStorage()) to also reset modal/animation flags (e.g., set
showStatsModal and showHelpModal to false and clear any animation-related
reactive flags), clear and cancel pendingKeyUpdates, cancel/clear any
notification timers, and abort in-flight async ops by wiring and invoking
AbortControllers used by submitWordStats() and loadDefinitionAndImage() (e.g.,
statsAbortController, definitionAbortController) so their late completions
cannot repopulate state; ensure any promises that update shared state check the
AbortSignal or a "currentGameId" token before applying results.
---
Outside diff comments:
In `@composables/useAnalytics.ts`:
- Around line 594-599: The trackInvalidWord implementation currently sends raw
user input (params.word) to PostHog; remove the literal word from the payload
and instead send derived features (e.g., word_length, script/category,
has_diacritics boolean, attempt_number, language) from within trackInvalidWord
so no free‑form text is transmitted to the third party, and if you still need
the raw value for first‑party diagnostics store it in a separate internal-only
path (not via track). Update the call in trackInvalidWord (the track(...)
invocation) and the InvalidWordParams usage accordingly to compute and send only
those derived fields.
---
Nitpick comments:
In `@components/game/GameHeader.vue`:
- Line 5: Replace hardcoded English aria-label values in GameHeader.vue with
localized strings from langStore (e.g., use langStore.config?.ui?.howToPlay for
the help button, and langStore.config?.ui?.statistics and
langStore.config?.ui?.settings for the other buttons referenced at lines 38 and
59); update the template attribute bindings to use these langStore properties
with sensible fallbacks (e.g., a short English string) so aria-label reads from
the language config while preserving the existing click handlers/emitters like
the help button's `@click`="$emit('help')".
In `@components/game/GameKeyboard.vue`:
- Line 2: The hardcoded aria-label "Keyboard" in the GameKeyboard.vue root div
should be localized; update the element to bind aria-label to your i18n
translation function (e.g., use $t or the composition API t) instead of the
static string so screen readers receive the locale-specific label—locate the div
with role="group" aria-label="Keyboard" and replace the literal with a
translated key like t('keyboard.label') (or inject/prop the localized string)
and ensure the translation key exists in your locale files.
In `@pages/`[lang]/word/[id].vue:
- Around line 536-544: The template contains hardcoded UI strings "Win Rate" and
"Avg Attempts" near the avgAttempts binding; replace them with keys from the
language config (e.g., use langStore.config?.ui?.winRate and
langStore.config?.ui?.avgAttempts or similar names) and update any corresponding
config/interface so these keys exist; ensure you provide safe fallbacks (default
English strings) when langStore.config is undefined and update any
tests/components that expect these strings.
In `@pages/`[lang]/words.vue:
- Around line 311-312: The localized UI labels may be undefined causing empty
output; update the template bindings around the plays and win labels to use safe
fallbacks (e.g., nullish coalescing) so that langStore.config?.ui?.plays and
langStore.config?.ui?.win default to a sensible string like "plays" and "wins"
when missing; change the expressions where w.stats.total and winRate(w.stats)
are rendered to append the fallback labels instead of relying on the optional
config only, ensuring the UI shows readable text even if langStore.config is not
loaded.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: b046a07d-e326-472a-a972-ec85fbc74c68
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (35)
README.mdassets/css/main.csscomponents/game/GameBoard.vuecomponents/game/GameHeader.vuecomponents/game/GameKeyboard.vuecomponents/game/HelpModal.vuecomponents/game/KeyboardKey.vuecomponents/game/NotificationToast.vuecomponents/game/SettingsModal.vuecomponents/game/StatsModal.vuecomponents/game/Tile.vuecomponents/game/TileRow.vuecomponents/shared/BaseModal.vuecomponents/shared/ToggleSwitch.vuecomposables/useAnalytics.tsdata/default_language_config.jsondocs/WORD_DATA_ARCHITECTURE.mddocs/nuxt-migration.mde2e/gameplay.spec.tslayouts/game.vuenuxt.config.tspackage.jsonpages/[lang]/index.vuepages/[lang]/word/[id].vuepages/[lang]/words.vuepages/accessibility.vueplugins/analytics.client.tsplugins/pwa.client.tsqa-screenshots.tsserver/api/[lang]/word-image/[word].get.tsserver/routes/t/[...path].tsserver/utils/definitions.tsstores/game.tsutils/game/useGameAnimations.tsutils/storage.ts
💤 Files with no reviewable changes (3)
- qa-screenshots.ts
- docs/WORD_DATA_ARCHITECTURE.md
- docs/nuxt-migration.md
| :data-char="char" | ||
| :aria-label="char" | ||
| @click="$emit('press', char)" | ||
| :aria-label="char === '⇨' ? 'Enter' : char === '⌫' ? 'Backspace' : keyAriaLabel" |
There was a problem hiding this comment.
Localize the ARIA label fragments.
Enter, Backspace, correct, present, absent, and hold for … are hard-coded in English here. On non-English keyboards the screen reader output will still be English even though the rest of the UI is localized.
Also applies to: 59-69
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@components/game/KeyboardKey.vue` at line 7, The ARIA label strings are
hard-coded in English; update the component to use the app's i18n translations
instead. Replace direct literals in the :aria-label binding (the char checks for
'⇨' and '⌫' and the keyAriaLabel usage) with calls to the translation function
(e.g., $t or the composition API t) such as $t('keyboard.enter'),
$t('keyboard.backspace') and for status labels replace
'correct'/'present'/'absent' with
$t('keyboard.status.correct')/$t('keyboard.status.present')/$t('keyboard.status.absent');
for the “hold for …” fragment use a translation with interpolation like
$t('keyboard.holdFor', { key: keyLabel }). Ensure any computed or method that
builds keyAriaLabel (and similar logic referenced in this file) uses these
translation keys so all ARIA output is localized.
| @touchstart.passive="handleTouchStart" | ||
| @touchmove.passive="handleTouchMove" | ||
| @touchend.prevent="handleTouchEnd" | ||
| @touchcancel="handleTouchCancel" |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
fd -e vue | grep -i keyboardRepository: Hugo0/wordle
Length of output: 120
🏁 Script executed:
cat components/game/KeyboardKey.vue | head -50Repository: Hugo0/wordle
Length of output: 1964
🏁 Script executed:
wc -l components/game/KeyboardKey.vueRepository: Hugo0/wordle
Length of output: 91
🏁 Script executed:
cat -n components/game/KeyboardKey.vue | sed -n '50,200p'Repository: Hugo0/wordle
Length of output: 5091
🏁 Script executed:
cat -n components/game/KeyboardKey.vue | sed -n '200,352p'Repository: Hugo0/wordle
Length of output: 4525
🌐 Web query:
touchend preventDefault suppress synthetic click event browser behavior
💡 Result:
Yes, calling preventDefault on the touchend event suppresses the browser's generation of the synthetic click event in modern browsers that emulate mouse events from touch input. According to the W3C Touch Events specification (result 58), if preventDefault is called on touchstart, touchmove, or touchend events, the user agent should not dispatch any mouse event (including click) that would be a consequential result of the prevented touch event. This applies to touchend as the touch sequence completes there. MDN documentation (results 8, 12) confirms: "Since calling preventDefault on a touchstart or the first touchmove event of a series prevents the corresponding mouse events from firing, it's common to call preventDefault on touchmove rather than touchstart. That way, mouse events can still fire and things like links will continue to work." However, multiple sources including Stack Overflow answers (result 2, highly voted) and practical implementations explicitly recommend and demonstrate using touchend preventDefault to suppress synthetic clicks while allowing scrolling (unlike touchstart which blocks scrolling). Chrome's developer blog (result 18) reinforces: "To suppress the default behavior of a tap (such as the generation of a click event), call preventDefault inside of a touchend listener." This behavior is consistent across major browsers (Chrome, Safari/iOS, Firefox) per W3C spec and testing reports, though older bugs existed (e.g., result 1). Use {passive: false} for touchmove/touchend listeners in Chrome if needed for preventDefault to take effect. Code example: element.addEventListener('touchend', function(event) { event.preventDefault; // Suppresses synthetic click/mouse events // Custom tap handling here }, { passive: false }); Prefer Pointer Events for new code to unify touch/mouse/pen handling.
Citations:
- 1: https://stackoverflow.com/questions/2890898/preventing-mouse-emulation-events-i-e-click-from-touch-events-in-mobile-safar
- 2: https://stackoverflow.com/questions/6780965/why-onclick-event-suppressed-when-preventdefault-is-called-for-the-touchstart
- 3: https://developer.mozilla.org/en-US/docs/Web/API/Touch_events
- 4: https://developer.mozilla.org/en-US/docs/Web/API/Touch_events/Using_Touch_Events
- 5: https://developer.chrome.com/blog/scrolling-intervention/
- 6: https://www.w3.org/TR/touch-events/
- 7: https://www.w3.org/community/reports/touchevents/CG-FINAL-touch-events-20240704/
Emit base characters on quick-tap touch events for variant keys.
Quick taps on diacritic-capable keys don't emit anything on real touch devices. @touchend.prevent suppresses the synthetic click, and handleTouchEnd() only emits when the popup opened—but popup takes 300ms to show, so quick taps complete before it appears. Add emit('press', props.char) to the else path when the popup never opened.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@components/game/KeyboardKey.vue` around lines 9 - 12, The touchend handler
currently only emits when the variant popup opened, so quick taps on
diacritic-capable keys produce no output; update handleTouchEnd to call
emit('press', props.char) in the else path when the popup never opened (i.e.,
when the popup open flag/state is false or its show timeout never fired) so
quick taps will emit the base character; reference handleTouchEnd, the popup
open state/timeout variable, and props.char when making this change.
| role="alert" | ||
| aria-live="assertive" | ||
| aria-atomic="true" |
There was a problem hiding this comment.
alert/assertive here can preempt guess-result announcements.
With the current flow, this toast (Line 4-6) can fire around the same time as the board’s polite live-region update, which may cause the row-result announcement to be interrupted for screen reader users.
Suggested adjustment
- role="alert"
- aria-live="assertive"
+ role="status"
+ aria-live="polite"
aria-atomic="true"🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@components/game/NotificationToast.vue` around lines 4 - 6, The toast
component NotificationToast.vue currently uses role="alert" and
aria-live="assertive" which can preempt other announcements; update the
NotificationToast element to use a less intrusive live region (e.g.,
role="status" and aria-live="polite") or remove the assertive announcement so it
won't interrupt the board’s polite live-region updates; keep aria-atomic if you
need whole-message updates but ensure NotificationToast's live-region settings
are changed from "alert/assertive" to "status/polite" (or equivalent) to avoid
interrupting screen reader output.
| const ariaLabel = computed(() => { | ||
| if (!props.letter) return 'Empty'; | ||
| const state = props.tileClass.includes('correct') && !props.tileClass.includes('semicorrect') | ||
| ? ', correct position' | ||
| : props.tileClass.includes('semicorrect') | ||
| ? ', wrong position' | ||
| : props.tileClass.includes('incorrect') | ||
| ? ', not in word' | ||
| : ''; | ||
| return `${props.letter}${state}`; | ||
| }); |
There was a problem hiding this comment.
ARIA labels are hardcoded in English in a multilingual game.
Line 19-27 will always announce English state text, which breaks screen-reader localization for non-English players. These labels should come from language config/store UI strings.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@components/game/Tile.vue` around lines 18 - 28, The computed ariaLabel
currently builds English state text inline in ariaLabel; replace those hardcoded
strings with localized UI strings from your i18n or language store (e.g., use
the translation function or reactive store values inside the computed) so the
state fragments (Empty, ", correct position", ", wrong position", ", not in
word") come from translations. Update the ariaLabel computed to pull localized
strings (e.g., t('tile.empty'), t('tile.correctPosition'),
t('tile.wrongPosition'), t('tile.notInWord')) and preserve the existing
tileClass checks (props.tileClass.includes('correct'), 'semicorrect',
'incorrect') and letter handling, ensuring a safe fallback to English or a
default translation if translation keys are missing.
components/shared/BaseModal.vue
Outdated
| v-show="visible" | ||
| role="dialog" | ||
| aria-modal="true" | ||
| :aria-labelledby="labelId" |
There was a problem hiding this comment.
Dialog accessible name should be mandatory.
aria-labelledby is wired, but labelId is optional/default undefined, so dialogs can ship without an accessible name.
Suggested fix
defineProps<{
visible: boolean;
size?: 'sm' | 'md' | 'lg';
zIndex?: 30 | 50;
noPadding?: boolean;
- labelId?: string;
+ labelId: string;
}>()Also applies to: 43-50
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@components/shared/BaseModal.vue` at line 6, Make the dialog accessible name
mandatory by updating BaseModal's props so that either labelId or ariaLabel must
be provided: add a props-level validator (or mark labelId as required) that
enforces (props.labelId || props.ariaLabel) is truthy and surface a clear dev
warning/error when neither is supplied; keep the template binding
:aria-labelledby="labelId" but allow the component to set aria-label when
ariaLabel is provided as the fallback. Reference the BaseModal component and the
labelId prop (and ariaLabel prop) when making this change so every modal
instance fails fast if no accessible name is supplied.
server/routes/t/[...path].ts
Outdated
| const contentLength = getHeader(event, 'content-length'); | ||
| if (contentLength && parseInt(contentLength, 10) > 1_000_000) { | ||
| setResponseStatus(event, 413); | ||
| return 'Payload too large'; | ||
| } |
There was a problem hiding this comment.
Header-only payload limiting is bypassable.
Current protection depends on content-length. Requests without that header (or malformed values) can still stream large bodies to the proxy.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@server/routes/t/`[...path].ts around lines 13 - 17, The current check relies
only on getHeader('content-length') so requests missing or with malformed
content-length bypass the limit; update the logic around contentLength,
parseInt, and setResponseStatus to enforce a hard body-size cap by actively
reading the request body stream and aborting when the accumulated bytes exceed
the 1_000_000 limit. Specifically: treat absent or non-numeric contentLength as
unknown (do not assume small), start consuming the incoming stream from event
(e.g., the raw request stream used by your framework), count bytes as they
arrive and call setResponseStatus(event, 413) and stop processing if the count
exceeds 1_000_000; retain the quick header-based check as an early exit but
always enforce the streaming byte-limit fallback for robustness.
server/routes/t/[...path].ts
Outdated
| const clientIp = getHeader(event, 'x-forwarded-for') || getRequestIP(event); | ||
|
|
||
| return proxyRequest(event, targetUrl, { | ||
| headers: { | ||
| host: hostname, | ||
| ...(clientIp ? { 'x-forwarded-for': clientIp } : {}), | ||
| }, |
There was a problem hiding this comment.
Do not forward untrusted x-forwarded-for as-is.
Using the inbound header directly lets clients spoof IP geolocation/attribution. Prefer forwarding only the server-resolved remote IP.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@server/routes/t/`[...path].ts around lines 23 - 29, The code is currently
taking the inbound 'x-forwarded-for' header via getHeader(event,
'x-forwarded-for') and forwarding it directly, allowing clients to spoof IPs;
change the logic in the t/[...path].ts route so that clientIp is derived only
from the server-resolved address (use getRequestIP(event) exclusively) and do
not forward the raw incoming header value. Update the proxyRequest headers
construction (where 'x-forwarded-for' is set) to send only the server-resolved
client IP (from getRequestIP) or omit the header if that value is absent;
reference getHeader, getRequestIP, proxyRequest, and the 'x-forwarded-for' usage
to locate and update the code.
| // If cached result is English-only (kaikki-en fallback), try LLM for native | ||
| if (loaded.source === 'kaikki-en' && !loaded.definition_native) { | ||
| // Fall through to LLM to get native definition | ||
| } else { | ||
| return loaded; | ||
| } |
There was a problem hiding this comment.
This can trigger repeated LLM calls on every request for the same word.
When cache contains source === 'kaikki-en' without definition_native, you always retry LLM. If LLM fails, the same cache shape is written again, so requests keep re-triggering expensive attempts.
Suggested fix (cooldown metadata for native-enrichment retries)
// cache read
if (loaded.source === 'kaikki-en' && !loaded.definition_native) {
- // Fall through to LLM to get native definition
+ const retryAt = loaded.native_retry_after || 0;
+ if (Date.now() / 1000 < retryAt) return loaded;
} else {
return loaded;
}
// cache write
+const nowTs = Math.floor(Date.now() / 1000);
+const cacheValue =
+ result?.source === 'kaikki-en' && !result.definition_native
+ ? { ...result, native_retry_after: nowTs + NEGATIVE_CACHE_TTL }
+ : result || { not_found: true, ts: nowTs };
writeFileSync(
cachePath,
- JSON.stringify(result || { not_found: true, ts: Math.floor(Date.now() / 1000) }),
+ JSON.stringify(cacheValue),
'utf-8'
);Also applies to: 307-314
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@server/utils/definitions.ts` around lines 287 - 292, The cache-check logic
currently always falls through to the LLM when a record has loaded.source ===
'kaikki-en' and no loaded.definition_native, causing repeated expensive retries;
modify the logic that guards the LLM call to consult cooldown metadata on the
cached object (e.g., add fields like loaded.nextNativeEnrichRetryAt or
loaded.nativeEnrichFailedCount) and only call the LLM when the cooldown has
expired or retry count is below a threshold; when the LLM attempt fails, write
back the cache with updated cooldown/next retry timestamp and incremented failed
count so subsequent requests skip LLM until the cooldown expires; apply the same
change to the analogous branch referenced at lines 307-314 so both
native-enrichment paths respect the retry cooldown metadata.
| // Announce guess result for screen readers | ||
| const rowTiles = tiles.value[revealingRow]; | ||
| const rowColors = tileColors.value[revealingRow]; | ||
| if (rowTiles && rowColors) { | ||
| const parts = rowTiles.map((letter, i) => { | ||
| const color = rowColors[i]; | ||
| const state = | ||
| color === 'correct' | ||
| ? 'correct' | ||
| : color === 'semicorrect' | ||
| ? 'present' | ||
| : 'absent'; | ||
| return `${letter} ${state}`; | ||
| }); | ||
| srAnnouncement.value = `Row ${revealingRow + 1}: ${parts.join(', ')}`; | ||
| } |
There was a problem hiding this comment.
Localize the live-region announcement before shipping it.
Row, correct, present, and absent are hard-coded, so screen-reader announcements on Hebrew/Arabic/Finnish/etc. will still come out in English. This new accessibility surface needs translated tokens/templates from the language config.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@stores/game.ts` around lines 535 - 550, The announcement currently hard-codes
English tokens ("Row", "correct", "present", "absent") when building
srAnnouncement in the block using tiles, tileColors and revealingRow; replace
those literals with lookups into the app's localization/language config (e.g.,
use the existing i18n/locale helper you use elsewhere) so you construct the
parts using translated labels for the row prefix and for each tile state, then
set srAnnouncement.value to a localized template (for example a translated "Row
{n}: {items}" pattern) so screen readers speak the localized strings instead of
English.
| /** Reset all game state to defaults. Called before loading a new language's game. */ | ||
| function resetGameState(): void { | ||
| tiles.value = makeEmptyGrid(MAX_GUESSES, WORD_LENGTH, ''); | ||
| tileColors.value = makeEmptyGrid(MAX_GUESSES, WORD_LENGTH, 'empty'); | ||
| tileClasses.value = makeEmptyGrid(MAX_GUESSES, WORD_LENGTH, DEFAULT_TILE_CLASS); | ||
| tilesVisual.value = makeEmptyGrid(MAX_GUESSES, WORD_LENGTH, ''); | ||
| tileClassesVisual.value = makeEmptyGrid(MAX_GUESSES, WORD_LENGTH, DEFAULT_TILE_CLASS); | ||
| activeRow.value = 0; | ||
| activeCell.value = 0; | ||
| fullWordInputted.value = false; | ||
| gameOver.value = false; | ||
| gameWon.value = false; | ||
| attempts.value = '0'; | ||
| keyClasses.value = {}; | ||
| pendingKeyUpdates.value = []; | ||
| emojiBoard.value = ''; | ||
| communityPercentile.value = null; | ||
| communityIsTopScore.value = false; | ||
| communityTotal.value = 0; | ||
| communityStatsLink.value = null; | ||
| shareButtonState.value = 'idle'; | ||
| srAnnouncement.value = ''; | ||
| todayDefinition.value = null; | ||
| todayImageUrl.value = null; | ||
| todayImageLoading.value = false; | ||
| todayDefinitionLoading.value = false; | ||
| maxDifficultyUsed.value = 0; | ||
| notification.value = makeEmptyNotification(); | ||
| } | ||
|
|
||
| /** Restore game state from localStorage. */ | ||
| function loadFromLocalStorage(): void { | ||
| if (!import.meta.client) return; | ||
| resetGameState(); |
There was a problem hiding this comment.
The language reset is still leaky for transient UI and async state.
resetGameState() clears the board arrays, but it leaves modal/animation flags untouched and doesn't stop existing timers/promises from writing old values back. After a language switch, stale showStatsModal/showHelpModal state, notification timers, or late submitWordStats() / loadDefinitionAndImage() completions can still repopulate the new page with previous-language data. pages/[lang]/index.vue only turns showStatsModal on in the gameOver branch, so a stale true survives the normal new-game path.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@stores/game.ts` around lines 910 - 943, resetGameState() currently clears
board arrays but not transient UI flags or ongoing async/timers; update
resetGameState() (and call it early in loadFromLocalStorage()) to also reset
modal/animation flags (e.g., set showStatsModal and showHelpModal to false and
clear any animation-related reactive flags), clear and cancel pendingKeyUpdates,
cancel/clear any notification timers, and abort in-flight async ops by wiring
and invoking AbortControllers used by submitWordStats() and
loadDefinitionAndImage() (e.g., statsAbortController, definitionAbortController)
so their late completions cannot repopulate state; ensure any promises that
update shared state check the AbortSignal or a "currentGameId" token before
applying results.
There was a problem hiding this comment.
Actionable comments posted: 12
♻️ Duplicate comments (4)
pages/accessibility.vue (1)
6-10:⚠️ Potential issue | 🟠 MajorAvoid compliance over-claim in metadata.
Line 9 still says “WCAG 2.1 AA compliant,” while Lines 68-80 document non-compliant areas. Please phrase this as a conformance target to prevent a compliance misstatement.
Suggested fix
useSeoMeta({ title: 'Accessibility — Wordle Global', description: - 'Wordle Global accessibility statement. WCAG 2.1 AA compliant word game in 79 languages with screen reader support, keyboard navigation, and reduced motion.', + 'Wordle Global accessibility statement. We target WCAG 2.1 Level AA conformance across 79 languages, with screen reader support, keyboard navigation, and reduced motion.', });🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@pages/accessibility.vue` around lines 6 - 10, The metadata created by useSeoMeta currently claims "WCAG 2.1 AA compliant" which is an over-claim; update the description passed to useSeoMeta (and any related title/description variables) to state it is a conformance target or goal (e.g., "aims to conform to WCAG 2.1 AA" or "WCAG 2.1 AA conformance target") instead of asserting compliance, so the page metadata aligns with the documented non-conformances.stores/game.ts (2)
535-550:⚠️ Potential issue | 🟠 MajorLocalize the screen reader announcement strings.
The announcement still uses hard-coded English tokens (
"Row","correct","present","absent"). For RTL languages (Hebrew, Arabic, etc.) and non-English speakers, these should be pulled from the language config to ensure screen readers announce in the user's language.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@stores/game.ts` around lines 535 - 550, The screen-reader announcement in the block using tiles, tileColors, revealingRow and srAnnouncement currently embeds hard-coded English tokens ("Row", "correct", "present", "absent"); replace those with localized strings from the app's i18n/language config (e.g. obtain a translator or language object and use keys like row label and state labels) and build the parts using the localized state names instead of the literal strings so announcements follow the user's locale and RTL ordering; ensure you call the existing translation function or language store where this file accesses translations and use those keys when composing srAnnouncement.
910-938:⚠️ Potential issue | 🟠 Major
resetGameStatedoes not cancel in-flight async operations.While
resetGameState()now properly clears reactive state and notification timers (viamakeEmptyNotification()), it doesn't abort in-flightloadDefinitionAndImage()orsubmitWordStats()calls. On rapid language switches, late completions could write stale data to the new game's state.Consider adding AbortControllers to these async operations and aborting them in
resetGameState().🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@stores/game.ts` around lines 910 - 938, resetGameState currently clears reactive state but doesn't cancel ongoing async ops like loadDefinitionAndImage or submitWordStats, so their late completions can overwrite new game state; add AbortController support: create and store per-operation controllers (e.g., definitionAbortController, statsAbortController) as refs used by loadDefinitionAndImage and submitWordStats (pass their signal into fetch/async calls and handle DOMAbortError), then in resetGameState call abort() on those controllers and reset them to null so in-flight promises are cancelled and cannot write stale data; also ensure any callers check for abort and return early and that controllers are re-created when starting a new operation.components/game/KeyboardKey.vue (1)
9-12:⚠️ Potential issue | 🔴 CriticalTouch taps still drop characters.
Same root cause as the earlier quick-tap report, but broader:
@touchend.preventis attached to every key, whilehandleTouchEnd()only emits after a popup selection. On a real touch screen that suppresses the click fallback, so non-variant keys and quick taps on variant keys type nothing.Minimal fix
function handleTouchEnd() { if (!props.variants?.length) { + emit('press', props.char); touchActive = false; return; } cancelLongPress(); touchActive = false; if (popupVisible.value) { selectAndDismiss(); + } else { + emit('press', props.char); } }Also applies to: 151-163
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@components/game/KeyboardKey.vue` around lines 9 - 12, Touch taps are being suppressed because `@touchend.prevent` is attached to every key while handleTouchEnd only prevents/fires for popup selections; remove the blanket .prevent and make touchend non-blocking: change the template listeners from `@touchend.prevent`="handleTouchEnd" to `@touchend`="handleTouchEnd" (apply the same change to the other block at lines 151-163), and update handleTouchEnd(event) to call event.preventDefault() only when handling a variant-popup selection (and otherwise emit the normal key event or allow the click fallback), leaving handleTouchStart/handleTouchMove/handleTouchCancel behavior unchanged.
🧹 Nitpick comments (2)
scripts/cleanup_language_configs.py (2)
77-80: Filter out non-directory entries in the iteration.
LANGUAGES_DIR.iterdir()may yield files (e.g.,.DS_Storeon macOS) in addition to directories. Whenlang_diris a file,lang_dir / "language_config.json"still produces a validPaththat won't exist, so the script works but wastes cycles and could produce confusing debug output if logging is added later.🔧 Suggested improvement
- for lang_dir in sorted(LANGUAGES_DIR.iterdir()): + for lang_dir in sorted(LANGUAGES_DIR.iterdir()): + if not lang_dir.is_dir(): + continue config_path = lang_dir / "language_config.json" if not config_path.exists(): continue🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@scripts/cleanup_language_configs.py` around lines 77 - 80, The loop over LANGUAGES_DIR.iterdir() should skip non-directory entries to avoid processing files like .DS_Store; update the iteration to check lang_dir.is_dir() (or filter only directories before looping) and continue when it's not a directory so that the subsequent creation and existence check of config_path (lang_dir / "language_config.json") only runs for actual language directories.
39-58: Consider clarifying in-place mutation in the docstring.The function mutates
configin place and returns the same reference. This is fine but could surprise callers expecting a new dict.📝 Suggested docstring update
def cleanup_config(config: dict) -> tuple[dict, int, int]: - """Clean a single config dict. Returns (cleaned_config, keys_removed, keys_renamed).""" + """Clean a single config dict in place. + + Returns (config, keys_removed, keys_renamed) where config is the same + mutated dict that was passed in. + """ removed = 0🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@scripts/cleanup_language_configs.py` around lines 39 - 58, The docstring for cleanup_config() is misleading because the function mutates the passed-in config dict in place and returns the same reference; update the docstring of cleanup_config to explicitly state that it modifies the input dict in place and returns the same dict along with counts (or, if preferred, change implementation to work on and return a copy instead). Reference the function name cleanup_config and the returned tuple (cleaned_config, keys_removed, keys_renamed) so callers know whether they must copy the config before calling or can rely on in-place changes.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@components/game/GameKeyboard.vue`:
- Around line 2-5: The div with role="group" in GameKeyboard.vue uses a
hardcoded aria-label="Keyboard" which won't localize; change it to bind to the
i18n translation (e.g., use :aria-label="$t('a11y.keyboard')" or a computed prop
that returns the localized string) and add the corresponding translation key
(e.g., a11y.keyboard or keyboard.label) to your locale files so the group name
is announced in the active language.
In `@components/game/KeyboardKey.vue`:
- Around line 281-286: The new .diacritic-popup animation (and the option
highlight transitions around lines for .diacritic-option / diacritic-popup-in
keyframes) ignores reduced-motion; add a prefers-reduced-motion guard to disable
animations/transitions for these selectors so users who set reduced motion get
no animation—specifically, target .diacritic-popup, the diacritic-popup-in
keyframes usage, and the option highlight selector (.diacritic-option or
whatever class is used for option highlight) and set animation: none and
transition: none inside `@media` (prefers-reduced-motion: reduce) to fully opt
them out.
- Around line 173-177: The current early-return in handleMouseDown that checks
'ontouchstart' incorrectly blocks mouse/trackpad on hybrid devices; replace the
mousedown flow with the Pointer Events API by changing the component binding
from `@mousedown` to `@pointerdown`, update the handler signature to accept a
PointerEvent in handleMouseDown, and check event.pointerType !== 'touch'
(instead of testing window.ontouchstart) while retaining the existing e.button
and props.variants checks so only real touch input is excluded and
mouse/trackpad work on 2-in-1 devices.
In `@data/languages/de/language_config.json`:
- Around line 32-34: The German language_config.json is missing the
"notification_word_not_valid" key, causing a fallback to English; add a new
entry "notification_word_not_valid" alongside "notification_copied" and
"notification_partial_word" in the same JSON object with an appropriate German
translation (e.g., "Das Wort ist ungültig" or your preferred wording), ensuring
the key name exactly matches "notification_word_not_valid" and the JSON
formatting (commas/quotes) stays valid.
In `@data/languages/en/language_config.json`:
- Around line 16-17: LanguageText in utils/types.ts still lists the old
kebab-case properties ('notification-copied', 'notification-partial-word');
update that interface to replace those keys with the new snake_case keys
(notification_copied, notification_partial_word) so the type matches
language_config.json and the usage in stores/game.ts (line ~457) and elsewhere;
ensure you only rename the property keys in the LanguageText interface (preserve
types/signatures) so IDE/typechecker recognizes notification_copied and
notification_partial_word.
In `@data/languages/fur/language_config.json`:
- Around line 19-20: The two localization values notification_copied and
notification_partial_word contain trailing spaces causing awkward UI and
screen-reader output; remove the trailing whitespace from the string values for
those keys in language_config.json (update the values for notification_copied
and notification_partial_word to not end with a space) and run the
localization/lint checks to ensure no other strings have accidental trailing
spaces.
In `@data/languages/id/language_config.json`:
- Around line 23-25: The LanguageText interface in utils/types.ts still declares
kebab-case keys causing a type mismatch with the JSON; update the interface
(LanguageText) to replace 'notification-copied' with 'notification_copied' and
'notification-partial-word' with 'notification_partial_word', and add an
optional 'notification_word_not_valid?: string;' property so the TypeScript
definition matches the snake_case keys used in the Indonesian JSON.
In `@data/languages/ko/language_config.json`:
- Around line 131-190: diacritic_map is missing an entry for the jamo "ㅒ" even
though physical_key_map maps ShiftKeyO to "ㅒ", so add a diacritic_map entry for
"ㅒ" that normalizes it to its compatibility jamo (same form used for other
entries, e.g., map "ㅒ" -> "ᅤ") so inputs from ShiftKeyO normalize/match
correctly; update the diacritic_map object to include the "ㅒ" key.
In `@data/languages/mk/language_config.json`:
- Around line 20-21: Update the LanguageText interface in utils/types.ts to use
the underscore key names that the language configs now provide: replace the old
hyphenated members 'notification-copied' and 'notification-partial-word' with
'notification_copied' and 'notification_partial_word' so the type definition
matches the actual config files (e.g., data/languages/mk/language_config.json)
and the usage in stores/game.ts; ensure the interface member names exactly match
the keys used across language config JSONs.
In `@data/languages/mn/language_config.json`:
- Around line 20-21: Type definitions and scripts still reference hyphenated
keys ("notification-copied", "notification-partial-word") whereas the JSON uses
snake_case ("notification_copied", "notification_partial_word"); update
utils/types.ts (the type/interface that declares language keys) to use
notification_copied and notification_partial_word, and update all occurrences in
scripts/add_language.py (around the spots you noted) and
scripts/cleanup_language_configs.py to use the snake_case property names so
runtime lookups match the JSON; search for the old hyphenated strings and
replace them with the new snake_case identifiers, and run tests or a quick parse
to confirm no remaining references.
In `@data/languages/nds/language_config.json`:
- Around line 19-20: Replace the English values for the keys notification_copied
and notification_partial_word with Low German translations so the nds locale is
consistent; update the JSON values for "notification_copied" and
"notification_partial_word" (e.g., use Low German strings such as "In de
Tussenablage kopiert" for copied-to-clipboard and "Bitte geef een heel Woot in"
for please-enter-a-full-word) ensuring proper JSON string quoting.
In `@data/languages/nn/language_config.json`:
- Around line 25-31: The Nynorsk language file contains English placeholders;
replace the English values for keys next_word, no_attempts, notification_copied,
notification_partial_word (and verify
share/share_challenge_win/share_challenge_lose) with proper Norwegian Nynorsk
strings and fully translate the entire help section to Nynorsk (use the nb file
as a reference for meaning but convert to Nynorsk), ensuring
formatting/placeholder tokens (e.g., {n}) and trailing spaces remain consistent
with other entries.
---
Duplicate comments:
In `@components/game/KeyboardKey.vue`:
- Around line 9-12: Touch taps are being suppressed because `@touchend.prevent` is
attached to every key while handleTouchEnd only prevents/fires for popup
selections; remove the blanket .prevent and make touchend non-blocking: change
the template listeners from `@touchend.prevent`="handleTouchEnd" to
`@touchend`="handleTouchEnd" (apply the same change to the other block at lines
151-163), and update handleTouchEnd(event) to call event.preventDefault() only
when handling a variant-popup selection (and otherwise emit the normal key event
or allow the click fallback), leaving
handleTouchStart/handleTouchMove/handleTouchCancel behavior unchanged.
In `@pages/accessibility.vue`:
- Around line 6-10: The metadata created by useSeoMeta currently claims "WCAG
2.1 AA compliant" which is an over-claim; update the description passed to
useSeoMeta (and any related title/description variables) to state it is a
conformance target or goal (e.g., "aims to conform to WCAG 2.1 AA" or "WCAG 2.1
AA conformance target") instead of asserting compliance, so the page metadata
aligns with the documented non-conformances.
In `@stores/game.ts`:
- Around line 535-550: The screen-reader announcement in the block using tiles,
tileColors, revealingRow and srAnnouncement currently embeds hard-coded English
tokens ("Row", "correct", "present", "absent"); replace those with localized
strings from the app's i18n/language config (e.g. obtain a translator or
language object and use keys like row label and state labels) and build the
parts using the localized state names instead of the literal strings so
announcements follow the user's locale and RTL ordering; ensure you call the
existing translation function or language store where this file accesses
translations and use those keys when composing srAnnouncement.
- Around line 910-938: resetGameState currently clears reactive state but
doesn't cancel ongoing async ops like loadDefinitionAndImage or submitWordStats,
so their late completions can overwrite new game state; add AbortController
support: create and store per-operation controllers (e.g.,
definitionAbortController, statsAbortController) as refs used by
loadDefinitionAndImage and submitWordStats (pass their signal into fetch/async
calls and handle DOMAbortError), then in resetGameState call abort() on those
controllers and reset them to null so in-flight promises are cancelled and
cannot write stale data; also ensure any callers check for abort and return
early and that controllers are re-created when starting a new operation.
---
Nitpick comments:
In `@scripts/cleanup_language_configs.py`:
- Around line 77-80: The loop over LANGUAGES_DIR.iterdir() should skip
non-directory entries to avoid processing files like .DS_Store; update the
iteration to check lang_dir.is_dir() (or filter only directories before looping)
and continue when it's not a directory so that the subsequent creation and
existence check of config_path (lang_dir / "language_config.json") only runs for
actual language directories.
- Around line 39-58: The docstring for cleanup_config() is misleading because
the function mutates the passed-in config dict in place and returns the same
reference; update the docstring of cleanup_config to explicitly state that it
modifies the input dict in place and returns the same dict along with counts
(or, if preferred, change implementation to work on and return a copy instead).
Reference the function name cleanup_config and the returned tuple
(cleaned_config, keys_removed, keys_renamed) so callers know whether they must
copy the config before calling or can rely on in-place changes.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: b764f54f-dd17-4fc1-929f-435b0b6a7e55
📒 Files selected for processing (88)
.gitignorecomponents/game/GameKeyboard.vuecomponents/game/KeyboardKey.vuecomponents/game/Tile.vuecomposables/useAnalytics.tsdata/default_language_config.jsondata/languages/ar/language_config.jsondata/languages/az/language_config.jsondata/languages/bg/language_config.jsondata/languages/bn/language_config.jsondata/languages/br/language_config.jsondata/languages/ca/language_config.jsondata/languages/ckb/language_config.jsondata/languages/cs/language_config.jsondata/languages/da/language_config.jsondata/languages/de/language_config.jsondata/languages/el/language_config.jsondata/languages/en/language_config.jsondata/languages/eo/language_config.jsondata/languages/es/language_config.jsondata/languages/et/language_config.jsondata/languages/eu/language_config.jsondata/languages/fa/language_config.jsondata/languages/fi/language_config.jsondata/languages/fo/language_config.jsondata/languages/fr/language_config.jsondata/languages/fur/language_config.jsondata/languages/fy/language_config.jsondata/languages/ga/language_config.jsondata/languages/gd/language_config.jsondata/languages/gl/language_config.jsondata/languages/ha/language_config.jsondata/languages/he/language_config.jsondata/languages/hi/language_config.jsondata/languages/hr/language_config.jsondata/languages/hu/language_config.jsondata/languages/hy/language_config.jsondata/languages/hyw/language_config.jsondata/languages/ia/language_config.jsondata/languages/id/language_config.jsondata/languages/ie/language_config.jsondata/languages/is/language_config.jsondata/languages/it/language_config.jsondata/languages/ja/language_config.jsondata/languages/ka/language_config.jsondata/languages/ko/language_config.jsondata/languages/la/language_config.jsondata/languages/lb/language_config.jsondata/languages/lt/language_config.jsondata/languages/ltg/language_config.jsondata/languages/lv/language_config.jsondata/languages/mi/language_config.jsondata/languages/mk/language_config.jsondata/languages/mn/language_config.jsondata/languages/mr/language_config.jsondata/languages/ms/language_config.jsondata/languages/nb/language_config.jsondata/languages/nds/language_config.jsondata/languages/ne/language_config.jsondata/languages/nl/language_config.jsondata/languages/nn/language_config.jsondata/languages/oc/language_config.jsondata/languages/pa/language_config.jsondata/languages/pau/language_config.jsondata/languages/pl/language_config.jsondata/languages/pt/language_config.jsondata/languages/qya/language_config.jsondata/languages/ro/language_config.jsondata/languages/ru/language_config.jsondata/languages/rw/language_config.jsondata/languages/sk/language_config.jsondata/languages/sl/language_config.jsondata/languages/sq/language_config.jsondata/languages/sr/language_config.jsondata/languages/sv/language_config.jsondata/languages/sw/language_config.jsondata/languages/tk/language_config.jsondata/languages/tl/language_config.jsondata/languages/tlh/language_config.jsondata/languages/tr/language_config.jsondata/languages/uk/language_config.jsondata/languages/ur/language_config.jsondata/languages/uz/language_config.jsondata/languages/vi/language_config.jsondata/languages/yo/language_config.jsonpages/accessibility.vuescripts/cleanup_language_configs.pystores/game.ts
✅ Files skipped from review due to trivial changes (1)
- data/languages/sv/language_config.json
🚧 Files skipped from review as they are similar to previous changes (1)
- components/game/Tile.vue
| <div | ||
| role="group" | ||
| aria-label="Keyboard" | ||
| class="flex flex-col container mx-auto gap-2 w-full max-w-lg pb-2 md:pb-5 px-2" |
There was a problem hiding this comment.
Localize the keyboard group's accessible name.
aria-label="Keyboard" will still be announced in English on non-English routes. Route this through the same localization path as the other accessibility strings so the group name matches the active language.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@components/game/GameKeyboard.vue` around lines 2 - 5, The div with
role="group" in GameKeyboard.vue uses a hardcoded aria-label="Keyboard" which
won't localize; change it to bind to the i18n translation (e.g., use
:aria-label="$t('a11y.keyboard')" or a computed prop that returns the localized
string) and add the corresponding translation key (e.g., a11y.keyboard or
keyboard.label) to your locale files so the group name is announced in the
active language.
| function handleMouseDown(e: MouseEvent) { | ||
| if (!props.variants?.length) return; | ||
| if (e.button !== 0) return; | ||
| if ('ontouchstart' in window) return; | ||
|
|
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
find . -name "KeyboardKey.vue" -type fRepository: Hugo0/wordle
Length of output: 89
🏁 Script executed:
cat -n ./components/game/KeyboardKey.vue | sed -n '165,185p'Repository: Hugo0/wordle
Length of output: 728
🏁 Script executed:
cat -n ./components/game/KeyboardKey.vue | sed -n '1,50p'Repository: Hugo0/wordle
Length of output: 2314
🏁 Script executed:
cat -n ./components/game/KeyboardKey.vue | sed -n '173,220p'Repository: Hugo0/wordle
Length of output: 1547
🏁 Script executed:
cat -n ./components/game/KeyboardKey.vue | sed -n '120,172p'Repository: Hugo0/wordle
Length of output: 1522
🏁 Script executed:
cat -n ./components/game/KeyboardKey.vue | wc -lRepository: Hugo0/wordle
Length of output: 59
🏁 Script executed:
cat -n ./components/game/KeyboardKey.vue | sed -n '50,120p'Repository: Hugo0/wordle
Length of output: 2698
🏁 Script executed:
cat -n ./components/game/KeyboardKey.vue | sed -n '220,280p'Repository: Hugo0/wordle
Length of output: 2018
Remove ontouchstart check or use Pointer Events API to distinguish input type.
On 2-in-1 devices (iPad, Surface), the ontouchstart property is present in the window object even when using a mouse or trackpad. The early return at line 176 prevents the desktop diacritic picker from working with mouse input on these hybrid devices, despite the handler being bound only to @mousedown.
Suggested change
function handleMouseDown(e: MouseEvent) {
if (!props.variants?.length) return;
if (e.button !== 0) return;
- if ('ontouchstart' in window) return;For a more robust solution, migrate to Pointer Events API: change @mousedown to @pointerdown, update the parameter to PointerEvent, and check pointerType !== 'touch' to exclude actual touch input while preserving mouse and trackpad functionality on hybrid devices.
📝 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.
| function handleMouseDown(e: MouseEvent) { | |
| if (!props.variants?.length) return; | |
| if (e.button !== 0) return; | |
| if ('ontouchstart' in window) return; | |
| function handleMouseDown(e: MouseEvent) { | |
| if (!props.variants?.length) return; | |
| if (e.button !== 0) return; |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@components/game/KeyboardKey.vue` around lines 173 - 177, The current
early-return in handleMouseDown that checks 'ontouchstart' incorrectly blocks
mouse/trackpad on hybrid devices; replace the mousedown flow with the Pointer
Events API by changing the component binding from `@mousedown` to `@pointerdown`,
update the handler signature to accept a PointerEvent in handleMouseDown, and
check event.pointerType !== 'touch' (instead of testing window.ontouchstart)
while retaining the existing e.button and props.variants checks so only real
touch input is excluded and mouse/trackpad work on 2-in-1 devices.
| .diacritic-popup { | ||
| position: fixed; | ||
| z-index: 9999; | ||
| pointer-events: none; | ||
| animation: diacritic-popup-in 0.15s cubic-bezier(0.2, 0.9, 0.3, 1.2); | ||
| } |
There was a problem hiding this comment.
The popup reintroduces motion for reduced-motion users.
This new animation path always animates the popup and option highlight, so it bypasses the reduced-motion support called out elsewhere in the PR.
CSS guard
.diacritic-popup {
position: fixed;
z-index: 9999;
pointer-events: none;
animation: diacritic-popup-in 0.15s cubic-bezier(0.2, 0.9, 0.3, 1.2);
}
+
+@media (prefers-reduced-motion: reduce) {
+ .diacritic-popup {
+ animation: none;
+ }
+
+ .diacritic-option {
+ transition: none;
+ }
+}Also applies to: 315-316, 342-351
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@components/game/KeyboardKey.vue` around lines 281 - 286, The new
.diacritic-popup animation (and the option highlight transitions around lines
for .diacritic-option / diacritic-popup-in keyframes) ignores reduced-motion;
add a prefers-reduced-motion guard to disable animations/transitions for these
selectors so users who set reduced motion get no animation—specifically, target
.diacritic-popup, the diacritic-popup-in keyframes usage, and the option
highlight selector (.diacritic-option or whatever class is used for option
highlight) and set animation: none and transition: none inside `@media`
(prefers-reduced-motion: reduce) to fully opt them out.
| "share_challenge_lose": "Ich habe das heutige Wordle nicht geschafft. Schaffst du es?", | ||
| "notification_copied": "In die Zwischenablage kopiert. ", | ||
| "notification_partial_word": "Bitte geben Sie ein vollständiges Wort ein! " |
There was a problem hiding this comment.
Missing notification_word_not_valid key.
The German config has notification_copied and notification_partial_word, but is missing notification_word_not_valid. This will cause the app to fall back to English ("Word is not valid") for German users. Other language configs (ja, ms) include this key.
🌐 Proposed fix to add the missing key
"share_challenge_lose": "Ich habe das heutige Wordle nicht geschafft. Schaffst du es?",
"notification_copied": "In die Zwischenablage kopiert. ",
+ "notification_word_not_valid": "Das Wort ist nicht gültig",
"notification_partial_word": "Bitte geben Sie ein vollständiges Wort ein! "📝 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.
| "share_challenge_lose": "Ich habe das heutige Wordle nicht geschafft. Schaffst du es?", | |
| "notification_copied": "In die Zwischenablage kopiert. ", | |
| "notification_partial_word": "Bitte geben Sie ein vollständiges Wort ein! " | |
| "share_challenge_lose": "Ich habe das heutige Wordle nicht geschafft. Schaffst du es?", | |
| "notification_copied": "In die Zwischenablage kopiert. ", | |
| "notification_word_not_valid": "Das Wort ist nicht gültig", | |
| "notification_partial_word": "Bitte geben Sie ein vollständiges Wort ein! " |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@data/languages/de/language_config.json` around lines 32 - 34, The German
language_config.json is missing the "notification_word_not_valid" key, causing a
fallback to English; add a new entry "notification_word_not_valid" alongside
"notification_copied" and "notification_partial_word" in the same JSON object
with an appropriate German translation (e.g., "Das Wort ist ungültig" or your
preferred wording), ensuring the key name exactly matches
"notification_word_not_valid" and the JSON formatting (commas/quotes) stays
valid.
| "notification_copied": "Copied to clipboard ", | ||
| "notification_partial_word": "Please enter a full word " |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Search for any remaining references to the old kebab-case keys
rg -n 'notification-copied|notification-partial-word' --type vue --type ts --type jsRepository: Hugo0/wordle
Length of output: 83
🏁 Script executed:
#!/bin/bash
# First, check what file types exist in the repository
echo "=== File types in repository ==="
fd -e js -e ts -e vue | head -20
echo -e "\n=== Search for OLD kebab-case keys ==="
rg 'notification-copied|notification-partial-word' -n
echo -e "\n=== Search for NEW snake_case keys ==="
rg 'notification_copied|notification_partial_word' -nRepository: Hugo0/wordle
Length of output: 19859
🏁 Script executed:
#!/bin/bash
# Check what imports/uses the types from utils/types.ts
echo "=== Files importing from utils/types.ts ==="
rg 'from.*utils/types|import.*types' -n | grep -v node_modules | head -20
# Check if add_language.py is actually used
echo -e "\n=== Check add_language.py context ==="
head -20 scripts/add_language.py
# Check if the Python script generates the JSON files
echo -e "\n=== Check if add_language.py is executed ==="
rg 'add_language' -n --type py --type jsonRepository: Hugo0/wordle
Length of output: 2809
🏁 Script executed:
#!/bin/bash
# View the Notification interface/type definition
echo "=== Notification type definition ==="
sed -n '20,40p' utils/types.ts
# Also check how it's used in stores/game.ts around line 457
echo -e "\n=== Usage in stores/game.ts (context around line 457) ==="
sed -n '450,465p' stores/game.ts
# Check if there are any type errors or TODOs related to this
echo -e "\n=== Check for TODOs or type-related comments ==="
rg 'TODO|FIXME|TODO-KEY|notification' utils/types.ts -C 2Repository: Hugo0/wordle
Length of output: 1296
🏁 Script executed:
#!/bin/bash
# Get the full context of what the 'text' property should be in LanguageConfig
echo "=== Full LanguageConfig interface ==="
rg 'interface LanguageConfig' -A 30 utils/types.ts
# Check if there's a LanguageConfigText or similar interface
echo -e "\n=== Check for text-related interfaces ==="
rg 'interface.*Text|interface.*Config' utils/types.ts -A 5Repository: Hugo0/wordle
Length of output: 1829
🏁 Script executed:
#!/bin/bash
# Get the complete LanguageText interface
echo "=== Full LanguageText interface ==="
rg 'export interface LanguageText' -A 20 utils/types.tsRepository: Hugo0/wordle
Length of output: 563
Update utils/types.ts to complete the key migration:
The LanguageText interface in utils/types.ts still defines the old kebab-case keys ('notification-copied', 'notification-partial-word') while the code in stores/game.ts:457 and all language config files use the new snake_case variants (notification_copied, notification_partial_word). Replace the kebab-case properties with their snake_case equivalents to maintain type safety across the codebase.
Required type definition update:
export interface LanguageText {
subheader: string;
next_word: string;
no_attempts: string;
share: string;
shared?: string;
copied?: string;
- 'notification-copied'?: string;
- 'notification-partial-word'?: string;
+ notification_copied?: string;
+ notification_partial_word?: string;
}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@data/languages/en/language_config.json` around lines 16 - 17, LanguageText in
utils/types.ts still lists the old kebab-case properties ('notification-copied',
'notification-partial-word'); update that interface to replace those keys with
the new snake_case keys (notification_copied, notification_partial_word) so the
type matches language_config.json and the usage in stores/game.ts (line ~457)
and elsewhere; ensure you only rename the property keys in the LanguageText
interface (preserve types/signatures) so IDE/typechecker recognizes
notification_copied and notification_partial_word.
| "ㅏ": [ | ||
| "ᅡ" | ||
| ], | ||
| "ㅐ": [ | ||
| "ᅢ" | ||
| ], | ||
| "ㅑ": [ | ||
| "ᅣ" | ||
| ], | ||
| "ㅓ": [ | ||
| "ᅥ" | ||
| ], | ||
| "ㅔ": [ | ||
| "ᅦ" | ||
| ], | ||
| "ㅕ": [ | ||
| "ᅧ" | ||
| ], | ||
| "ㅖ": [ | ||
| "ᅨ" | ||
| ], | ||
| "ㅗ": [ | ||
| "ᅩ" | ||
| ], | ||
| "ㅘ": [ | ||
| "ᅪ" | ||
| ], | ||
| "ㅙ": [ | ||
| "ᅫ" | ||
| ], | ||
| "ㅚ": [ | ||
| "ᅬ" | ||
| ], | ||
| "ㅛ": [ | ||
| "ᅭ" | ||
| ], | ||
| "ㅜ": [ | ||
| "ᅮ" | ||
| ], | ||
| "ㅝ": [ | ||
| "ᅯ" | ||
| ], | ||
| "ㅞ": [ | ||
| "ᅰ" | ||
| ], | ||
| "ㅟ": [ | ||
| "ᅱ" | ||
| ], | ||
| "ㅠ": [ | ||
| "ᅲ" | ||
| ], | ||
| "ㅡ": [ | ||
| "ᅳ" | ||
| ], | ||
| "ㅢ": [ | ||
| "ᅴ" | ||
| ], | ||
| "ㅣ": [ | ||
| "ᅵ" | ||
| ] |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Verify physical key outputs are covered by diacritic_map for Korean config.
python - <<'PY'
import json
from pathlib import Path
p = Path("data/languages/ko/language_config.json")
cfg = json.loads(p.read_text(encoding="utf-8"))
physical_values = set(cfg.get("physical_key_map", {}).values())
diacritic_keys = set(cfg.get("diacritic_map", {}).keys())
missing = sorted(physical_values - diacritic_keys)
print("Missing diacritic_map keys for physical_key_map outputs:", missing)
PYRepository: Hugo0/wordle
Length of output: 118
🏁 Script executed:
cat -n data/languages/ko/language_config.json | sed -n '120,230p'Repository: Hugo0/wordle
Length of output: 2699
🏁 Script executed:
python3 << 'EOF'
import unicodedata
# Check the characters
chars = ['ㅒ', 'ᅤ']
for char in chars:
print(f"Character: {char}")
print(f" Code point: U+{ord(char):04X}")
print(f" Name: {unicodedata.name(char, 'UNKNOWN')}")
print()
# Check if there's a decomposition
print("Decomposition info:")
s = "ㅒ"
decomp = unicodedata.decomposition(s)
print(f" ㅒ (U+{ord(s):04X}): {decomp or 'No decomposition'}")
# Compare with other entries in the diacritic_map to understand the pattern
mappings = {
'ㅏ': 'ᅡ',
'ㅐ': 'ᅢ',
'ㅑ': 'ᅣ',
'ㅓ': 'ᅥ',
'ㅔ': 'ᅦ',
}
print("\nExisting mappings pattern:")
for k, v in mappings.items():
k_code = ord(k)
v_code = ord(v)
print(f" {k} (U+{k_code:04X}) -> {v} (U+{v_code:04X})")
# Check ㅒ in relation to the pattern
print(f"\nㅒ code: U+{ord('ㅒ'):04X}")
print(f"Expected equivalent ᅤ code: U+{ord('ᅤ'):04X}")
EOFRepository: Hugo0/wordle
Length of output: 442
Add missing ㅒ mapping in diacritic_map to match physical_key_map.
Line 224 maps ShiftKeyO to ㅒ, but diacritic_map (lines 131-190) has no ㅒ entry. This breaks normalization/matching for inputs using ㅒ.
🔧 Proposed fix
"ㅑ": [
"ᅣ"
],
+ "ㅒ": [
+ "ᅤ"
+ ],
"ㅓ": [
"ᅥ"
],📝 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.
| "ㅏ": [ | |
| "ᅡ" | |
| ], | |
| "ㅐ": [ | |
| "ᅢ" | |
| ], | |
| "ㅑ": [ | |
| "ᅣ" | |
| ], | |
| "ㅓ": [ | |
| "ᅥ" | |
| ], | |
| "ㅔ": [ | |
| "ᅦ" | |
| ], | |
| "ㅕ": [ | |
| "ᅧ" | |
| ], | |
| "ㅖ": [ | |
| "ᅨ" | |
| ], | |
| "ㅗ": [ | |
| "ᅩ" | |
| ], | |
| "ㅘ": [ | |
| "ᅪ" | |
| ], | |
| "ㅙ": [ | |
| "ᅫ" | |
| ], | |
| "ㅚ": [ | |
| "ᅬ" | |
| ], | |
| "ㅛ": [ | |
| "ᅭ" | |
| ], | |
| "ㅜ": [ | |
| "ᅮ" | |
| ], | |
| "ㅝ": [ | |
| "ᅯ" | |
| ], | |
| "ㅞ": [ | |
| "ᅰ" | |
| ], | |
| "ㅟ": [ | |
| "ᅱ" | |
| ], | |
| "ㅠ": [ | |
| "ᅲ" | |
| ], | |
| "ㅡ": [ | |
| "ᅳ" | |
| ], | |
| "ㅢ": [ | |
| "ᅴ" | |
| ], | |
| "ㅣ": [ | |
| "ᅵ" | |
| ] | |
| "ㅏ": [ | |
| "ᅡ" | |
| ], | |
| "ㅐ": [ | |
| "ᅢ" | |
| ], | |
| "ㅑ": [ | |
| "ᅣ" | |
| ], | |
| "ㅒ": [ | |
| "ᅤ" | |
| ], | |
| "ㅓ": [ | |
| "ᅥ" | |
| ], | |
| "ㅔ": [ | |
| "ᅦ" | |
| ], | |
| "ㅕ": [ | |
| "ᅧ" | |
| ], | |
| "ㅖ": [ | |
| "ᅨ" | |
| ], | |
| "ㅗ": [ | |
| "ᅩ" | |
| ], | |
| "ㅘ": [ | |
| "ᅪ" | |
| ], | |
| "ㅙ": [ | |
| "ᅫ" | |
| ], | |
| "ㅚ": [ | |
| "ᅬ" | |
| ], | |
| "ㅛ": [ | |
| "ᅭ" | |
| ], | |
| "ㅜ": [ | |
| "ᅮ" | |
| ], | |
| "ㅝ": [ | |
| "ᅯ" | |
| ], | |
| "ㅞ": [ | |
| "ᅰ" | |
| ], | |
| "ㅟ": [ | |
| "ᅱ" | |
| ], | |
| "ㅠ": [ | |
| "ᅲ" | |
| ], | |
| "ㅡ": [ | |
| "ᅳ" | |
| ], | |
| "ㅢ": [ | |
| "ᅴ" | |
| ], | |
| "ㅣ": [ | |
| "ᅵ" | |
| ] |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@data/languages/ko/language_config.json` around lines 131 - 190, diacritic_map
is missing an entry for the jamo "ㅒ" even though physical_key_map maps ShiftKeyO
to "ㅒ", so add a diacritic_map entry for "ㅒ" that normalizes it to its
compatibility jamo (same form used for other entries, e.g., map "ㅒ" -> "ᅤ") so
inputs from ShiftKeyO normalize/match correctly; update the diacritic_map object
to include the "ㅒ" key.
| "notification_copied": "Копирани во таблата со исечоци ", | ||
| "notification_partial_word": "Ве молиме внесете целосен збор " |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Verify all language configs use the new underscore-based key names
echo "=== Checking for old hyphenated key names ==="
rg -n "notification-copied|notification-partial-word" data/languages/
echo ""
echo "=== Checking code references to old key names ==="
rg -n "notification-copied|notification-partial-word" --type vue --type ts --type js
echo ""
echo "=== Checking if removed UI keys are still referenced ==="
rg -n "haptic_feedback|sound_effects|easy_mode" --type vue --type ts --type js
echo ""
echo "=== Verifying new key names exist in code ==="
rg -n "notification_copied|notification_partial_word" --type vue --type ts --type js -C2Repository: Hugo0/wordle
Length of output: 358
🏁 Script executed:
#!/bin/bash
# Check for old hyphenated key names in language configs
echo "=== Language config files present ==="
find data/languages -name "*.json" | head -20
echo ""
echo "=== Checking for old hyphenated key names in language configs ==="
rg "notification-copied|notification-partial-word" data/languages/ || echo "No old hyphenated key names found"
echo ""
echo "=== Checking code references to old key names ==="
rg "notification-copied|notification-partial-word" --type javascript --type typescript || echo "No old key references in JS/TS code"
echo ""
echo "=== Checking if removed UI keys are referenced in code ==="
rg "haptic_feedback|sound_effects|easy_mode" --type javascript --type typescript || echo "No references to removed UI keys"
echo ""
echo "=== Verifying new key names are used in code ==="
rg "notification_copied|notification_partial_word" --type javascript --type typescript -C 2Repository: Hugo0/wordle
Length of output: 1241
🏁 Script executed:
#!/bin/bash
# Search code without file type restrictions
echo "=== Searching entire codebase for old hyphenated keys ==="
rg "notification-copied|notification-partial-word" || echo "No matches found"
echo ""
echo "=== Searching for removed UI keys in entire codebase ==="
rg "haptic_feedback|sound_effects|easy_mode" || echo "No matches found"
echo ""
echo "=== Searching for new key names in entire codebase ==="
rg "notification_copied|notification_partial_word" -C 2
echo ""
echo "=== Check a sample of other language configs for key name consistency ==="
echo "--- en language config keys ---"
rg '"notification_' data/languages/en/language_config.json || rg '"notification-' data/languages/en/language_config.json || echo "English config not found or keys missing"
echo ""
echo "=== Checking mk config file for the exact keys mentioned ==="
cat -n data/languages/mk/language_config.json | grep -A 2 -B 2 "notification"Repository: Hugo0/wordle
Length of output: 50368
🏁 Script executed:
#!/bin/bash
# Check utils/types.ts and see if it's a source of truth
echo "=== Checking utils/types.ts structure ==="
head -50 utils/types.ts
echo ""
echo "=== Checking if types.ts is exported/imported widely ==="
rg "from.*utils/types|import.*from.*types" -A 2 -B 2 | head -50
echo ""
echo "=== Check mk language config to verify underscore keys ==="
grep -A 10 "notification" data/languages/mk/language_config.jsonRepository: Hugo0/wordle
Length of output: 4598
Update type definitions in utils/types.ts to match language config key renaming.
The LanguageText interface in utils/types.ts still defines the old hyphenated key names ('notification-copied' and 'notification-partial-word'), but the language config files—including the Macedonian config at data/languages/mk/language_config.json—now use underscores (notification_copied, notification_partial_word). Additionally, stores/game.ts already references the new underscore key name.
This type definition mismatch will cause TypeScript type checking failures. Update the interface to use the new underscore-based key names to match the actual config structure.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@data/languages/mk/language_config.json` around lines 20 - 21, Update the
LanguageText interface in utils/types.ts to use the underscore key names that
the language configs now provide: replace the old hyphenated members
'notification-copied' and 'notification-partial-word' with 'notification_copied'
and 'notification_partial_word' so the type definition matches the actual config
files (e.g., data/languages/mk/language_config.json) and the usage in
stores/game.ts; ensure the interface member names exactly match the keys used
across language config JSONs.
| "notification_copied": "Санах самбар руу хуулсан ", | ||
| "notification_partial_word": "Бүрэн үг оруулна уу " |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Description: Search for any remaining references to the old hyphenated key names.
# Check for old key references in the codebase (excluding language config JSON files themselves)
echo "Searching for 'notification-copied':"
rg -n "notification-copied" --type-not json
echo ""
echo "Searching for 'notification-partial-word':"
rg -n "notification-partial-word" --type-not json
echo ""
echo "Also checking within JSON files for inconsistent naming:"
rg -n "notification-copied|notification-partial-word" --type jsonRepository: Hugo0/wordle
Length of output: 2318
Critical: Type definitions and utility code still reference old hyphenated keys.
The snake_case normalization in the JSON is correct, but the TypeScript type definitions in utils/types.ts (lines 23–24) and utility scripts still reference the old hyphenated keys (notification-copied, notification-partial-word). This mismatch will cause runtime lookup failures.
- Update
utils/types.tsto use snake_case keys - Update
scripts/add_language.py(lines 86, 88, 183, 185, 253, 255, etc.) to use the new key names - Update
scripts/cleanup_language_configs.pyto match the new naming
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@data/languages/mn/language_config.json` around lines 20 - 21, Type
definitions and scripts still reference hyphenated keys ("notification-copied",
"notification-partial-word") whereas the JSON uses snake_case
("notification_copied", "notification_partial_word"); update utils/types.ts (the
type/interface that declares language keys) to use notification_copied and
notification_partial_word, and update all occurrences in scripts/add_language.py
(around the spots you noted) and scripts/cleanup_language_configs.py to use the
snake_case property names so runtime lookups match the JSON; search for the old
hyphenated strings and replace them with the new snake_case identifiers, and run
tests or a quick parse to confirm no remaining references.
| "notification_copied": "Copied to clipboard ", | ||
| "notification_partial_word": "Please enter a full word " |
There was a problem hiding this comment.
Translate notification strings for the nds locale.
Line 19 and Line 20 are still English (“Copied to clipboard”, “Please enter a full word”), which creates mixed-language UX in Low German.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@data/languages/nds/language_config.json` around lines 19 - 20, Replace the
English values for the keys notification_copied and notification_partial_word
with Low German translations so the nds locale is consistent; update the JSON
values for "notification_copied" and "notification_partial_word" (e.g., use Low
German strings such as "In de Tussenablage kopiert" for copied-to-clipboard and
"Bitte geef een heel Woot in" for please-enter-a-full-word) ensuring proper JSON
string quoting.
| "next_word": "Next word ", | ||
| "no_attempts": "You haven't tried any words yet! ", | ||
| "share": "Del", | ||
| "notification-copied": "Copied to clipboard ", | ||
| "notification-partial-word": "Please enter a full word ", | ||
| "share_challenge_win": "Eg løyste dagens Wordle på {n} forsøk. Klarar du det betre?", | ||
| "share_challenge_lose": "Eg klarte ikkje dagens Wordle. Klarar du det?" | ||
| "share_challenge_lose": "Eg klarte ikkje dagens Wordle. Klarar du det?", | ||
| "notification_copied": "Copied to clipboard ", | ||
| "notification_partial_word": "Please enter a full word " |
There was a problem hiding this comment.
Missing Norwegian Nynorsk translations — English placeholder text remains.
Several strings in this file are still in English instead of Norwegian Nynorsk:
- Line 25:
"next_word": "Next word " - Line 26:
"no_attempts": "You haven't tried any words yet! " - Line 30:
"notification_copied": "Copied to clipboard " - Line 31:
"notification_partial_word": "Please enter a full word "
Additionally, the entire help section (lines 37-47) contains English text. Compare to the Norwegian Bokmål (nb) file which has proper Norwegian translations for these same keys.
This will result in a mixed-language experience for Nynorsk users.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@data/languages/nn/language_config.json` around lines 25 - 31, The Nynorsk
language file contains English placeholders; replace the English values for keys
next_word, no_attempts, notification_copied, notification_partial_word (and
verify share/share_challenge_win/share_challenge_lose) with proper Norwegian
Nynorsk strings and fully translate the entire help section to Nynorsk (use the
nb file as a reference for meaning but convert to Nynorsk), ensuring
formatting/placeholder tokens (e.g., {n}) and trailing spaces remain consistent
with other entries.
- Remove 10 dead translation keys across 79 language configs (222 keys removed)
- Rename 3 kebab-case keys to snake_case (176 renames)
- Add 20+ new UI translation keys (difficulty, community stats, hard mode, etc.)
- Bulk translate missing keys for 52 languages via LLM
- Wire up hardcoded English strings to translation system
- Win modal now shows native language definitions (not English)
- Hard mode messages now translatable with {letter} and {position} placeholders
- 27 remaining languages fall back to English defaults (no regression)
…trailing spaces - Proxy: use getRequestIP() instead of client x-forwarded-for (prevents spoofing) - Proxy: removed content-length-only check (proxyRequest handles streaming) - Definitions: kaikki-en cache retries LLM only once per 24h (prevents repeated calls) - Definitions: kaikki results now include timestamp for cache expiry - Fixed trailing spaces in 60 language config files - README: 78 → 79 languages
… use readLocal/writeLocal in getOrCreateId
Summary
direction: rtlinstead of array reversal. Affects all 5 RTL languages (he, ar, fa, ckb, ur). Should fix Hebrew's 11% completion rate.resetGameState()+ page-levelkeyfor full remount on lang change./tserver endpoint to defeat ad blockers. Migrated from manual init to@posthog/nuxtmodule.Bug Fixes
Accessibility (zero visual change)
role="alert"+aria-live="assertive"on notification toastaria-labelon all header buttons + keyboard keys with statearia-labelwith state ("A, correct position")aria-liveregion for guess result announcementsrole="dialog"+aria-modalon modalsrole="grid"/role="row"/role="gridcell"on game boardprefers-reduced-motiondisables all animations (CSS + JS)focus-visiblerings/accessibilitypage with WCAG statementrole="switch"on toggle switchesAnalytics
@posthog/nuxtmodule +/tproxy routeinvalid_wordevents now include the word (capped 10 chars)$exceptioncaptureCode Quality
isStandalone()+getOrCreateId()to shared utilsTest plan
prefers-reduced-motiondisables animations@coderabbitai full review
Summary by CodeRabbit
New Features
Accessibility
Localization
Analytics
Tests
Documentation