feat: Auto-detect browser language on homepage#116
Conversation
Detect the user's browser language via navigator.language and show a prominent "Play" button in their language above the language grid. Reduces friction for the 6,800 weekly homepage visitors who currently have to scan through 65+ language cards to find theirs. - Add detectBrowserLanguage() to index-app.ts - Add hero CTA section to index.html (hidden when no match) - Add UI translation keys for CTA text - Prettier formatting fixes (analytics.ts, game.ts, playwright.config.ts)
|
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. 📝 WalkthroughWalkthroughAdds browser language detection and a conditional CTA in the language selection UI; introduces a new Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (2)
webapp/templates/index.html (1)
99-105: Consider consolidating the twov-if="detectedLanguage"sibling elements.Lines 86–97 and 100–102 are separate root-level
v-if="detectedLanguage"blocks. Wrapping them in a single<template v-if="detectedLanguage">eliminates the duplicated condition check and makes the conditional scope explicit.♻️ Proposed refactor
- <!-- Auto-detected language CTA --> - <div v-if="detectedLanguage" class="flex flex-col items-center mt-8 mb-2 px-4"> + <!-- Auto-detected language CTA --> + <template v-if="detectedLanguage"> + <div class="flex flex-col items-center mt-8 mb-2 px-4"> ... - </div> - - <!-- "Or choose another language" label when CTA is shown --> - <p v-if="detectedLanguage" class="text-xs text-neutral-500 dark:text-neutral-400 text-center mt-6 mb-1"> + </div> + <!-- "Or choose another language" label when CTA is shown --> + <p class="text-xs text-neutral-500 dark:text-neutral-400 text-center mt-6 mb-1"> {{ ui.or_choose_language or "Or choose another language" }} - </p> + </p> + </template>🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@webapp/templates/index.html` around lines 99 - 105, The two sibling elements using the same condition (v-if="detectedLanguage") should be consolidated into a single conditional block: wrap the paragraph node that displays "{{ ui.or_choose_language or 'Or choose another language' }}" and the following div with the language search box (the element using :class="detectedLanguage ? 'mt-2' : 'mt-12'") inside one <template v-if="detectedLanguage"> so the check occurs once and the conditional scope is explicit around both nodes; update any surrounding spacing or indentation accordingly.frontend/src/index-app.ts (1)
150-171: No analytics event fires when the detected-language CTA is clicked.
selectLanguageWithCode()navigates directly without callingtrackLanguageSelect(). Since the entire purpose of this feature is to reduce friction for ~6,800 sessions/week, the lack of a distinct analytics source makes it impossible to measure actual impact versus organic language-grid clicks.Consider emitting a
trackLanguageSelectevent with a new'detected'source before navigation, and extending the union type accordingly.📊 Suggested addition in
index-app.ts// In analytics.ts — extend the source union: -export const trackLanguageSelect = (language: string, source: 'search' | 'list' | 'flag'): void => { +export const trackLanguageSelect = (language: string, source: 'search' | 'list' | 'flag' | 'detected'): void => {Then in
index.html, replace the inlinev-on:click:-<button v-on:click="selectLanguageWithCode(detectedLanguage.language_code)" +<button v-on:click="trackAndSelectLanguage(detectedLanguage.language_code)"And add the method in
index-app.ts:+ trackAndSelectLanguage(language_code: string): void { + import('./analytics').then(({ trackLanguageSelect }) => { + trackLanguageSelect(language_code, 'detected'); + this.selectLanguageWithCode(language_code); + }); + },🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/src/index-app.ts` around lines 150 - 171, When the detected-language CTA triggers navigation, ensure an analytics event is emitted: add 'detected' to the Source union/type used by trackLanguageSelect, and from the detected-language click handler call trackLanguageSelect with source 'detected' before invoking selectLanguageWithCode (or update selectLanguageWithCode to accept an optional source param and call trackLanguageSelect(source) when provided). Locate selectLanguageWithCode, trackLanguageSelect, and detectBrowserLanguage in index-app.ts to implement this change and update any index.html v-on:click used by the CTA to pass the 'detected' source if you choose the parameter approach.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@frontend/src/index-app.ts`:
- Around line 159-166: The build fails because prefix (from lower.split('-')[0])
can be undefined under noUncheckedIndexedAccess; fix by ensuring prefix is a
string before using it to index this.languages: extract the first part into a
const (e.g., parts = lower.split('-')) and either coalesce it to an empty string
(parts[0] ?? '') or add a type guard (if (typeof prefix === 'string' && prefix))
before the lookups; update the checks that reference prefix (the
this.languages[prefix] lookup and the prefix === 'no' special case) to run only
after the guard so the compiler knows prefix cannot be undefined.
In `@webapp/templates/index.html`:
- Around line 86-97: The subtitle is server-rendered via the Jinja expression
ui.daily_word_game causing an English-only string; change it to be populated
client-side so it matches detectedLanguage. Replace the Jinja expression
(ui.daily_word_game or "The daily word game") with a client-side binding that
reads the localized string from your Language/detectedLanguage object or from a
window.localizedStrings map (e.g., use detectedLanguage.daily_word_game or a
getDailyWordGameText() method invoked in the template) so the subtitle updates
after language detection; alternatively remove the subtitle element until a
localized value is available. Ensure you update any related code paths that
reference detectedLanguage and selectLanguageWithCode to maintain consistency.
- Around line 90-93: The button uses a hard-coded right arrow (→) which
will point the wrong way for RTL languages; update the template and/or model so
the arrow is not direction-specific: either remove the decorative arrow from the
button that displays [[ detectedLanguage.language_name_native ]] (no template
change needed beyond deleting the arrow), or add RTL-awareness by extending the
front-end Language interface in index-app.ts to include a right_to_left boolean
and then conditionally render/flip the arrow around the
selectLanguageWithCode(...) button (use detectedLanguage.right_to_left to choose
no arrow or the opposite arrow direction). Ensure you update references to
detectedLanguage and the Language interface accordingly so the template can
decide arrow visibility/direction.
---
Nitpick comments:
In `@frontend/src/index-app.ts`:
- Around line 150-171: When the detected-language CTA triggers navigation,
ensure an analytics event is emitted: add 'detected' to the Source union/type
used by trackLanguageSelect, and from the detected-language click handler call
trackLanguageSelect with source 'detected' before invoking
selectLanguageWithCode (or update selectLanguageWithCode to accept an optional
source param and call trackLanguageSelect(source) when provided). Locate
selectLanguageWithCode, trackLanguageSelect, and detectBrowserLanguage in
index-app.ts to implement this change and update any index.html v-on:click used
by the CTA to pass the 'detected' source if you choose the parameter approach.
In `@webapp/templates/index.html`:
- Around line 99-105: The two sibling elements using the same condition
(v-if="detectedLanguage") should be consolidated into a single conditional
block: wrap the paragraph node that displays "{{ ui.or_choose_language or 'Or
choose another language' }}" and the following div with the language search box
(the element using :class="detectedLanguage ? 'mt-2' : 'mt-12'") inside one
<template v-if="detectedLanguage"> so the check occurs once and the conditional
scope is explicit around both nodes; update any surrounding spacing or
indentation accordingly.
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
frontend/src/index-app.ts (1)
151-169:try-catchscope is broader than necessary.Only
navigator.languageaccess requires protection; the subsequentthis.languageslookups andsplitcall can't throw. Wrapping the entire body silently swallows any genuine runtime errors that might be introduced here in the future.♻️ Proposed narrower scope
detectBrowserLanguage(): Language | null { - try { - const browserLang = navigator.language || ''; - // ... rest of logic ... - } catch { - // navigator.language unavailable - } - return null; + let browserLang: string; + try { + browserLang = navigator.language || ''; + } catch { + return null; // navigator.language unavailable + } + const lower = browserLang.toLowerCase(); + // ... rest of logic outside try-catch ... + return null; },🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/src/index-app.ts` around lines 151 - 169, The try-catch is too broad: narrow it to only protect the navigator.language access by reading navigator.language inside a small try block (e.g., let browserLang = ''; try { browserLang = navigator.language || ''; } catch { browserLang = ''; }) and then perform the lowercasing, split, and this.languages lookups (references: navigator.language, browserLang, lower, prefix, this.languages) outside the catch so genuine runtime errors in the lookup logic aren’t silently swallowed; keep the same matching logic (exact, prefix, and 'no' → 'nb') but move it after the try.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@frontend/src/index-app.ts`:
- Around line 150-171: Update detectBrowserLanguage to iterate
navigator.languages (if available) in addition to navigator.language: loop the
array of language tags, normalize to lowercase, check exact matches against
this.languages, then try prefix matches (tag.split('-')[0]), and apply the
special mappings (map 'iw' to 'he' and map 'no' to 'nb') before returning null;
keep the existing try/catch and fallback behavior so older browsers still work.
Ensure the changes are made inside the detectBrowserLanguage method and keep
using this.languages and the same return type Language | null.
---
Nitpick comments:
In `@frontend/src/index-app.ts`:
- Around line 151-169: The try-catch is too broad: narrow it to only protect the
navigator.language access by reading navigator.language inside a small try block
(e.g., let browserLang = ''; try { browserLang = navigator.language || ''; }
catch { browserLang = ''; }) and then perform the lowercasing, split, and
this.languages lookups (references: navigator.language, browserLang, lower,
prefix, this.languages) outside the catch so genuine runtime errors in the
lookup logic aren’t silently swallowed; keep the same matching logic (exact,
prefix, and 'no' → 'nb') but move it after the try.
| detectBrowserLanguage(): Language | null { | ||
| try { | ||
| const browserLang = navigator.language || ''; | ||
| const lower = browserLang.toLowerCase(); | ||
| // Try exact match first (e.g. "nb" → "nb") | ||
| if (this.languages[lower]) { | ||
| return this.languages[lower] as Language; | ||
| } | ||
| // Try prefix match (e.g. "de-AT" → "de", "pt-BR" → "pt") | ||
| const prefix = lower.split('-')[0] ?? ''; | ||
| if (prefix && this.languages[prefix]) { | ||
| return this.languages[prefix] as Language; | ||
| } | ||
| // Special case: Norwegian "no" → "nb" (Bokmål) | ||
| if (prefix === 'no' && this.languages['nb']) { | ||
| return this.languages['nb'] as Language; | ||
| } | ||
| } catch { | ||
| // navigator.language unavailable | ||
| } | ||
| return null; | ||
| }, |
There was a problem hiding this comment.
Consider navigator.languages for better language matching coverage.
The current implementation only checks navigator.language (the primary language). For users whose primary locale isn't in the game (e.g., zh) but whose second preference is (e.g., fr), no CTA will appear.
♻️ Proposed enhancement using `navigator.languages`
- detectBrowserLanguage(): Language | null {
- try {
- const browserLang = navigator.language || '';
- const lower = browserLang.toLowerCase();
- // Try exact match first (e.g. "nb" → "nb")
- if (this.languages[lower]) {
- return this.languages[lower] as Language;
- }
- // Try prefix match (e.g. "de-AT" → "de", "pt-BR" → "pt")
- const prefix = lower.split('-')[0] ?? '';
- if (prefix && this.languages[prefix]) {
- return this.languages[prefix] as Language;
- }
- // Special case: Norwegian "no" → "nb" (Bokmål)
- if (prefix === 'no' && this.languages['nb']) {
- return this.languages['nb'] as Language;
- }
- } catch {
- // navigator.language unavailable
- }
- return null;
- },
+ detectBrowserLanguage(): Language | null {
+ try {
+ const langs: readonly string[] =
+ navigator.languages?.length ? navigator.languages : [navigator.language ?? ''];
+ for (const browserLang of langs) {
+ const lower = browserLang.toLowerCase();
+ // Try exact match (e.g. "nb" → "nb")
+ if (this.languages[lower]) {
+ return this.languages[lower] as Language;
+ }
+ // Try prefix match (e.g. "de-AT" → "de", "pt-BR" → "pt")
+ const prefix = lower.split('-')[0] ?? '';
+ if (prefix && this.languages[prefix]) {
+ return this.languages[prefix] as Language;
+ }
+ // Special case: Norwegian "no" → "nb" (Bokmål)
+ if (prefix === 'no' && this.languages['nb']) {
+ return this.languages['nb'] as Language;
+ }
+ // Legacy ISO 639-1 code: "iw" → "he" (Hebrew)
+ if (prefix === 'iw' && this.languages['he']) {
+ return this.languages['he'] as Language;
+ }
+ }
+ } catch {
+ // navigator.language/languages unavailable
+ }
+ return null;
+ },Minor: iw legacy Hebrew code not mapped.
Some browsers (particularly older Windows locale settings) still report iw (the deprecated ISO 639-1 code) instead of he for Hebrew. If Hebrew is in the game's language list, those users will silently get no CTA. The fix is included in the proposed diff above.
Based on learnings: "Consider RTL (right-to-left) language support for Hebrew, Arabic, and Persian in UI code."
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| detectBrowserLanguage(): Language | null { | |
| try { | |
| const browserLang = navigator.language || ''; | |
| const lower = browserLang.toLowerCase(); | |
| // Try exact match first (e.g. "nb" → "nb") | |
| if (this.languages[lower]) { | |
| return this.languages[lower] as Language; | |
| } | |
| // Try prefix match (e.g. "de-AT" → "de", "pt-BR" → "pt") | |
| const prefix = lower.split('-')[0] ?? ''; | |
| if (prefix && this.languages[prefix]) { | |
| return this.languages[prefix] as Language; | |
| } | |
| // Special case: Norwegian "no" → "nb" (Bokmål) | |
| if (prefix === 'no' && this.languages['nb']) { | |
| return this.languages['nb'] as Language; | |
| } | |
| } catch { | |
| // navigator.language unavailable | |
| } | |
| return null; | |
| }, | |
| detectBrowserLanguage(): Language | null { | |
| try { | |
| const langs: readonly string[] = | |
| navigator.languages?.length ? navigator.languages : [navigator.language ?? '']; | |
| for (const browserLang of langs) { | |
| const lower = browserLang.toLowerCase(); | |
| // Try exact match (e.g. "nb" → "nb") | |
| if (this.languages[lower]) { | |
| return this.languages[lower] as Language; | |
| } | |
| // Try prefix match (e.g. "de-AT" → "de", "pt-BR" → "pt") | |
| const prefix = lower.split('-')[0] ?? ''; | |
| if (prefix && this.languages[prefix]) { | |
| return this.languages[prefix] as Language; | |
| } | |
| // Special case: Norwegian "no" → "nb" (Bokmål) | |
| if (prefix === 'no' && this.languages['nb']) { | |
| return this.languages['nb'] as Language; | |
| } | |
| // Legacy ISO 639-1 code: "iw" → "he" (Hebrew) | |
| if (prefix === 'iw' && this.languages['he']) { | |
| return this.languages['he'] as Language; | |
| } | |
| } | |
| } catch { | |
| // navigator.language/languages unavailable | |
| } | |
| return null; | |
| }, |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@frontend/src/index-app.ts` around lines 150 - 171, Update
detectBrowserLanguage to iterate navigator.languages (if available) in addition
to navigator.language: loop the array of language tags, normalize to lowercase,
check exact matches against this.languages, then try prefix matches
(tag.split('-')[0]), and apply the special mappings (map 'iw' to 'he' and map
'no' to 'nb') before returning null; keep the existing try/catch and fallback
behavior so older browsers still work. Ensure the changes are made inside the
detectBrowserLanguage method and keep using this.languages and the same return
type Language | null.
Instead of a big green button above the grid, the detected browser language is now moved to the first position in the language card grid. Removes unused translation keys (daily_word_game, or_choose_language).
Falls through the user's full language preference list instead of only checking the primary language. Helps users whose primary locale isn't available but who have a secondary one that is.
feat: Auto-detect browser language on homepage
Summary
navigator.languageand shows a prominent green "Play" CTA in their native language name above the language gridWhy: The homepage gets ~6,800 sessions/week but avg duration is only 52 seconds. Most visitors just need to find their language in a wall of 65 cards. This puts their language one click away.
Files changed
frontend/src/index-app.tsdetectBrowserLanguage()method +detectedLanguagedatawebapp/templates/index.htmlwebapp/data/default_language_config.jsonfrontend/src/analytics.ts,game.ts,playwright.config.tsQA checklist
wordle.global— you should see a green button with your browser's language (probably "English →" or your system language)zh) — CTA should disappear, page looks like beforeSummary by CodeRabbit
New Features
Style