Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion frontend/src/analytics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -508,7 +508,12 @@ export const initErrorTracking = (language: string): void => {
* Track game abandonment on page unload
*/
export const initAbandonTracking = (
getState: () => { language: string; activeRow: number; gameOver: boolean; lastGuessValid: boolean }
getState: () => {
language: string;
activeRow: number;
gameOver: boolean;
lastGuessValid: boolean;
}
): void => {
window.addEventListener('beforeunload', () => {
const state = getState();
Expand Down
19 changes: 14 additions & 5 deletions frontend/src/game.ts
Original file line number Diff line number Diff line change
Expand Up @@ -697,8 +697,11 @@ export const createGameApp = () => {
// Track game completion with session-aggregated frustration state
const langCode = this.config?.language_code || 'unknown';
const frustrationState = analytics.resetFrustrationState();
const gameStartTime = (window as Window & { _gameStartTime?: number })._gameStartTime;
const timeToComplete = gameStartTime ? Math.floor((Date.now() - gameStartTime) / 1000) : undefined;
const gameStartTime = (window as Window & { _gameStartTime?: number })
._gameStartTime;
const timeToComplete = gameStartTime
? Math.floor((Date.now() - gameStartTime) / 1000)
: undefined;

analytics.trackGameComplete({
language: langCode,
Expand Down Expand Up @@ -735,8 +738,11 @@ export const createGameApp = () => {
// Track game completion (loss) with session-aggregated frustration state
const lossLangCode = this.config?.language_code || 'unknown';
const lossFrustrationState = analytics.resetFrustrationState();
const lossGameStartTime = (window as Window & { _gameStartTime?: number })._gameStartTime;
const lossTimeToComplete = lossGameStartTime ? Math.floor((Date.now() - lossGameStartTime) / 1000) : undefined;
const lossGameStartTime = (window as Window & { _gameStartTime?: number })
._gameStartTime;
const lossTimeToComplete = lossGameStartTime
? Math.floor((Date.now() - lossGameStartTime) / 1000)
: undefined;

analytics.trackGameComplete({
language: lossLangCode,
Expand Down Expand Up @@ -1190,7 +1196,10 @@ export const createGameApp = () => {
} catch {
// localStorage unavailable
}
analytics.trackSettingsChange({ setting: 'haptics', value: this.hapticsEnabled });
analytics.trackSettingsChange({
setting: 'haptics',
value: this.hapticsEnabled,
});
});
},

Expand Down
46 changes: 45 additions & 1 deletion frontend/src/index-app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@
total_stats: {} as TotalStats,
game_results: {} as Record<string, GameResult[]>,
expandedLanguage: '' as string, // For stats modal expansion
detectedLanguage: null as Language | null,
};
},

Expand Down Expand Up @@ -125,6 +126,8 @@
this.total_stats = this.calculateTotalStats();
// Initialize languages with recently played first
this.languages_vis = this.getSortedLanguages();
// Detect browser language for hero CTA
this.detectedLanguage = this.detectBrowserLanguage();
},

mounted() {
Expand All @@ -144,6 +147,36 @@
}
},

detectBrowserLanguage(): Language | null {
try {
// Try all preferred languages (navigator.languages),
// falling back to navigator.language for older browsers
const candidates = navigator.languages?.length
? navigator.languages
: [navigator.language || ''];

for (const browserLang of candidates) {
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.languages unavailable
}
return null;
},
Comment on lines +150 to +178

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.


selectLanguageWithCode(language_code: string): void {
window.location.href = '/' + language_code;
},
Expand Down Expand Up @@ -286,7 +319,18 @@
return rankA - rankB;
});

return [...playedLanguages, ...unplayedLanguages];
const sorted = [...playedLanguages, ...unplayedLanguages];

// If we detected the browser language, move it to the front
if (this.detectedLanguage) {
const detectedCode = this.detectedLanguage.language_code;
const idx = sorted.findIndex((l) => l.language_code === detectedCode);
if (idx > 0) {
sorted.unshift(sorted.splice(idx, 1)[0]);

Check failure on line 329 in frontend/src/index-app.ts

View workflow job for this annotation

GitHub Actions / Build Check

Argument of type 'Language | undefined' is not assignable to parameter of type 'Language'.
}
}

return sorted;
},

hasPlayed(language_code: string): boolean {
Expand Down
23 changes: 11 additions & 12 deletions playwright.config.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { defineConfig, devices } from "@playwright/test";
import { defineConfig, devices } from '@playwright/test';

/**
* Playwright E2E test configuration for Wordle Global.
Expand All @@ -8,32 +8,31 @@ import { defineConfig, devices } from "@playwright/test";
* Run specific test: pnpm test:e2e -g "homepage"
*/
export default defineConfig({
testDir: "./e2e",
testDir: './e2e',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: "html",
reporter: 'html',
use: {
baseURL: "http://127.0.0.1:8000",
trace: "on-first-retry",
baseURL: 'http://127.0.0.1:8000',
trace: 'on-first-retry',
},
projects: [
{
name: "chromium",
use: { ...devices["Desktop Chrome"] },
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
// Mobile viewport for responsive testing
{
name: "mobile",
use: { ...devices["iPhone 13"] },
name: 'mobile',
use: { ...devices['iPhone 13'] },
},
],
// Local dev server - start before tests if not already running
webServer: {
command:
". venv/bin/activate && gunicorn --chdir webapp --bind 127.0.0.1:8000 app:app",
url: "http://127.0.0.1:8000",
command: '. venv/bin/activate && gunicorn --chdir webapp --bind 127.0.0.1:8000 app:app',
url: 'http://127.0.0.1:8000',
reuseExistingServer: !process.env.CI,
timeout: 30000,
},
Expand Down
Loading