Skip to content

feat: Auto-detect browser language on homepage#116

Merged
Hugo0 merged 4 commits into
mainfrom
feat/homepage-language-detection
Feb 21, 2026
Merged

feat: Auto-detect browser language on homepage#116
Hugo0 merged 4 commits into
mainfrom
feat/homepage-language-detection

Conversation

@Hugo0

@Hugo0 Hugo0 commented Feb 21, 2026

Copy link
Copy Markdown
Owner

Summary

  • Detects the user's browser language via navigator.language and shows a prominent green "Play" CTA in their native language name above the language grid
  • Falls back gracefully: if the browser language isn't available (e.g. Chinese), no CTA is shown and the page looks exactly as before
  • Existing language grid, search, modals, and stats are completely unchanged

Why: 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

File What
frontend/src/index-app.ts detectBrowserLanguage() method + detectedLanguage data
webapp/templates/index.html Hero CTA section above search box
webapp/data/default_language_config.json 2 new UI translation keys
frontend/src/analytics.ts, game.ts, playwright.config.ts Prettier formatting only

QA checklist

  • Open wordle.global — you should see a green button with your browser's language (probably "English →" or your system language)
  • Click the green button — should navigate to the correct language page
  • Change browser language to something unavailable (e.g. Chinese zh) — CTA should disappear, page looks like before
  • Verify the language grid below the CTA still works normally (search, click cards, streaks visible)
  • Check dark mode — CTA should look good in both themes
  • Check mobile — CTA should be full-width and tappable

Summary by CodeRabbit

  • New Features

    • Automatic browser language detection shows a call-to-action with a quick-select button for the detected language.
    • Two new UI labels added to improve the language selection display.
  • Style

    • Code formatting and quoting style tidied across the project for consistency.

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)
@coderabbitai

coderabbitai Bot commented Feb 21, 2026

Copy link
Copy Markdown

Warning

Rate limit exceeded

@Hugo0 has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 4 minutes and 0 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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.

📝 Walkthrough

Walkthrough

Adds browser language detection and a conditional CTA in the language selection UI; introduces a new detectedLanguage data property and detectBrowserLanguage() method. The rest of changes are formatting-only edits (type annotation formatting, line breaks, quoting) and two new UI strings in the default language config.

Changes

Cohort / File(s) Summary
Browser language detection & UI
frontend/src/index-app.ts, webapp/templates/index.html, webapp/data/default_language_config.json
Adds `detectedLanguage: Language
Analytics type-formatting
frontend/src/analytics.ts
Reformats the getState parameter return type for initAbandonTracking from a single-line object type to a multi-line object type; no runtime or behavioral changes.
Game formatting changes
frontend/src/game.ts
Breaks long expressions across multiple lines (game timing/analytics calls) without changing semantics or logic flow.
Playwright config style-only
playwright.config.ts
Style/quoting changes (double → single quotes) and minor formatting; no behavioral or config value changes.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Poem

🐰 I sniff the browser's tongue at dawn,
A little CTA hops across the lawn,
"The daily word game" whispers bright,
Pick your language, tap—delight! ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: auto-detecting the browser language and displaying it on the homepage. It is concise, clear, and directly reflects the core functionality added across multiple files.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/homepage-language-detection

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

❤️ Share

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (2)
webapp/templates/index.html (1)

99-105: Consider consolidating the two v-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 calling trackLanguageSelect(). 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 trackLanguageSelect event 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 inline v-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 (&rarr;) 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.

Comment thread frontend/src/index-app.ts Outdated
Comment thread webapp/templates/index.html Outdated
Comment thread webapp/templates/index.html Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
frontend/src/index-app.ts (1)

151-169: try-catch scope is broader than necessary.

Only navigator.language access requires protection; the subsequent this.languages lookups and split call 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.

Comment thread frontend/src/index-app.ts
Comment on lines +150 to +171
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;
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

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.

Suggested change
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.
@Hugo0
Hugo0 merged commit 71183d9 into main Feb 21, 2026
3 of 4 checks passed
@Hugo0
Hugo0 deleted the feat/homepage-language-detection branch February 21, 2026 12:59
Hugo0 added a commit that referenced this pull request Feb 22, 2026
feat: Auto-detect browser language on homepage
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant