AT Mot — a bilingual (EN/FR) daily word game native to the AT Protocol.
0

Configure Feed

Select the types of activity you want to include in your feed.

Merge fix/lang-switch-on-puzzle-page: language toggle works on puzzle pages

+77 -1
+14
src/config.ts
··· 103 103 return { lang, puzzleNumber }; 104 104 } 105 105 106 + /** 107 + * Where the language toggle should take you. Puzzle pages encode the language 108 + * in the URL (`/p/<lang>/<n>`), so the router re-derives the language from the 109 + * path on every render — switching `ctx.lang` alone leaves a puzzle page on its 110 + * original language. Returns the equivalent puzzle path in `lang`, or null when 111 + * the current path is language-independent (or already in `lang`) and a plain 112 + * re-render suffices. 113 + */ 114 + export function pathForLangSwitch(currentPath: string, lang: Lang): string | null { 115 + const puzzle = parsePuzzleTarget(currentPath); 116 + if (puzzle && puzzle.lang !== lang) return `/p/${lang}/${puzzle.puzzleNumber}`; 117 + return null; 118 + } 119 + 106 120 /** Deterministic rkey for a result record: `<lang>-<puzzleNumber>` (documented in the lexicon). */ 107 121 export function resultRkey(lang: Lang, puzzleNumber: number): string { 108 122 return `${lang}-${puzzleNumber}`;
+8 -1
src/main.ts
··· 1 1 import './ui/theme.css'; 2 - import { isLang, type Lang, parsePuzzleTarget } from './config.js'; 2 + import { isLang, type Lang, parsePuzzleTarget, pathForLangSwitch } from './config.js'; 3 3 import { 4 4 isOAuthCallback, 5 5 completeCallback, ··· 37 37 ctx.lang = lang; 38 38 localStorage.setItem(LANG_KEY, lang); 39 39 document.documentElement.lang = lang; 40 + // On a puzzle page the language lives in the URL, so switch the path too — 41 + // otherwise render() re-derives the old language and nothing visibly changes. 42 + const dest = pathForLangSwitch(location.pathname, lang); 43 + if (dest) { 44 + ctx.navigate(dest); 45 + return; 46 + } 40 47 render(); 41 48 }, 42 49
+17
tests/config.test.ts
··· 2 2 import { 3 3 puzzleTarget, 4 4 parsePuzzleTarget, 5 + pathForLangSwitch, 5 6 resultRkey, 6 7 daysSinceEpoch, 7 8 puzzleNumberFor, ··· 32 33 expect(parsePuzzleTarget('https://atmot.herve.bzh/p/FR/412')).toBeNull(); 33 34 expect(parsePuzzleTarget('https://atmot.herve.bzh/p/de/1')).toBeNull(); 34 35 expect(parsePuzzleTarget('https://atmot.herve.bzh/p/fr/0')).toBeNull(); 36 + }); 37 + }); 38 + 39 + describe('pathForLangSwitch — keep puzzle pages in sync with the lang toggle', () => { 40 + it('redirects a puzzle page to the same puzzle in the new language', () => { 41 + expect(pathForLangSwitch('/p/fr/1', 'en')).toBe('/p/en/1'); 42 + expect(pathForLangSwitch('/p/en/412', 'fr')).toBe('/p/fr/412'); 43 + }); 44 + 45 + it('returns null when the puzzle is already in the target language', () => { 46 + expect(pathForLangSwitch('/p/fr/1', 'fr')).toBeNull(); 47 + }); 48 + 49 + it('returns null on language-independent paths (home, about)', () => { 50 + expect(pathForLangSwitch('/', 'fr')).toBeNull(); 51 + expect(pathForLangSwitch('/about', 'en')).toBeNull(); 35 52 }); 36 53 }); 37 54
+38
tests/header.test.ts
··· 1 + // @vitest-environment happy-dom 2 + import { describe, expect, it, vi } from 'vitest'; 3 + import { renderHeader } from '../src/ui/header.js'; 4 + import { pathForLangSwitch } from '../src/config.js'; 5 + import type { Ctx } from '../src/ui/context.js'; 6 + 7 + function makeCtx(over: Partial<Ctx> = {}): Ctx { 8 + return { 9 + lang: 'fr', 10 + session: null, 11 + handle: null, 12 + pendingRecord: null, 13 + setLang() {}, 14 + navigate() {}, 15 + rerender() {}, 16 + async signInWith() {}, 17 + async signOut() {}, 18 + ...over, 19 + }; 20 + } 21 + 22 + describe('renderHeader language switch', () => { 23 + it('the EN button asks to switch to English', () => { 24 + const setLang = vi.fn(); 25 + const header = renderHeader(makeCtx({ setLang })); 26 + const en = [...header.querySelectorAll('button')].find((b) => b.textContent === 'EN'); 27 + expect(en).toBeDefined(); 28 + en!.click(); 29 + expect(setLang).toHaveBeenCalledWith('en'); 30 + }); 31 + 32 + // Regression: on a French puzzle page, switching to EN must route to the 33 + // English puzzle. Previously setLang only flipped ctx.lang and re-rendered, 34 + // so the router re-derived `fr` from the unchanged path and nothing changed. 35 + it('switching language on a puzzle page routes to the same puzzle in the new language', () => { 36 + expect(pathForLangSwitch('/p/fr/1', 'en')).toBe('/p/en/1'); 37 + }); 38 + });