Skip to content

Product-grade UI, light/dark themes and Bahasa Melayu + English - #21

Merged
aimanmalib merged 4 commits into
mainfrom
feat/ui-bilingual
Oct 20, 2026
Merged

aimanmalib merged 4 commits into
mainfrom
feat/ui-bilingual

Conversation

@aimanmalib

Copy link
Copy Markdown
Owner

Why

LearnPath is shown as proof work and will be presented to Malaysian users and judges. It looked like a template (emoji icons, plain cards), had a contrast bug on /roadmap, and was English only.

Before → after

Before After
Language English only Bahasa Melayu + English, switch in the top bar; first visit follows the browser language (ms → BM); choice saved in localStorage (try/catch) and mirrored to a cookie so the server renders the first paint in the same language
AI output Always English Every API route receives language and tells the model to answer in it; BM prompt asks for standard Malaysian spelling and lists Indonesian words to avoid
Look Emoji, Tailwind grays, no identity Colour tokens (teal + warm accent on paper/ink), Fraunces headings + Inter, lucide-react icons, logo mark, desktop nav + mobile tab bar
Themes Light only Light + dark, follows prefers-color-scheme, toggle applied before first paint (no flash)
Roadmap step numbers White on gray-200: 1.2:1 Muted ink on surface: 7.7:1 light / 8.2:1 dark
States Bare spinner, raw server English errors Skeleton loading, localised error notices with retry / open settings, empty states, focus styles, skip link, dialog focus trap
Quiz Answers advanced instantly, no feedback Difficulty + length options, per-question feedback with explanation, results with review list
Flashcards Long text could clip, deck had no end Card grows with text, end-of-deck summary
Roadmap Static "Mark as done" moves the current step forward

All routes and features are kept. The Concept Map is still sample data and says so.

Contrast (computed, not eyeballed)

tests/design/contrast.test.ts parses globals.css and checks every text/background token pair for WCAG AA (4.5:1 text, 3:1 UI) in both themes, and that the system-dark and toggle-dark token sets are identical. Lowest text pairs: ink-subtle on surface-2 4.95:1 (light) and 5.91:1 (dark). Input borders 3.00:1 (light) / 3.85:1 (dark).

Tests added

  • tests/i18n/messages.test.ts: every key in both languages (fails on a missing key), no empty strings, same {placeholders}, no common Indonesian words in BM, BM not copied from English, agreed terms (Kuiz, Kad imbas, Peta laluan pembelajaran, Tetapan, Kunci API).
  • tests/i18n/locale.test.ts: browser-language default, Accept-Language parsing, persistence, blocked storage.
  • tests/i18n/no-hardcoded-text.test.ts: fails on hard-coded words in JSX text.
  • tests/api/routes.test.ts: for all five routes, language: "ms" puts the Malaysian Malay instruction in the system prompt, "en"/unknown → English.
  • tests/lib/api-client.test.ts: the browser client sends the selected language.

I sabotaged the key-parity and Indonesian-word tests (deleted one BM key, inserted "harus") and the hard-coded-text test; each failed as expected.

How it was verified

  • npm run typecheck, npm run lint, npm test (244 tests), npm run build, all green locally on Node 22.
  • Ran next start in server-key mode against a real OpenAI-compatible model and drove it with Playwright WebKit using devices['iPhone 13'] and a 1440×900 desktop, in BM and EN, light and dark:
    • generated a quiz, flashcards and a roadmap on "Fotosintesis" (BM) and "Photosynthesis" (EN), answered the whole quiz, flipped and rated cards, marked a roadmap step done, checked the Progress page shows the 3 sessions;
    • BM output checked word by word for Malay vs English vs Indonesian markers: all BM outputs Malay with no Indonesian markers, all EN outputs English;
    • no page errors or console errors, no horizontal scroll at 390 px on any page;
    • settings: server-mode message; bring-your-own-key form (provider, show key, save, forget key) with /api/llm-config mocked; missing-key error opens settings with the BM reason;
    • language switch EN → BM sets <html lang="ms">, persists across reload, page title follows;
    • system dark preference applies with nothing stored; theme toggle switches both ways;
    • step-number contrast measured in the browser: lowest 5.9:1.

Screenshots are not added to the README in this PR.

Notes

  • Pages are now rendered per request (the layout reads the language cookie / Accept-Language) so the first paint is in the right language.
  • How closely a model follows the BM instruction depends on the model the visitor chooses.

🤖 Generated with Claude Code

aimanmalib and others added 4 commits October 20, 2026 09:46
All UI text now lives in src/i18n/messages/{en,ms}.ts. The `Messages`
type and a test both fail when a key is missing in either language, when
placeholders differ, or when the BM file uses common Indonesian words.

The first visit follows the browser language (ms -> BM). The choice is
stored in localStorage (wrapped in try/catch, in-memory fallback) and
mirrored to a cookie so the server renders the first paint in the same
language.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every API call sends `language: "en" | "ms"`. Each route appends a
language instruction to its system prompt and a short reminder to the
user message. The BM instruction asks for standard Malaysian spelling and
names Indonesian words to avoid. Unknown values fall back to English.

Tests cover every route (quiz, flashcards, roadmap, chat, analyze) and the
browser client.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Colour tokens in globals.css for light and dark (prefers-color-scheme
  plus a toggle that applies before first paint); Tailwind's default
  palette is switched off so every colour goes through a token. A test
  computes WCAG AA contrast for every text/background pair in both themes.
- Fixes the roadmap contrast bug: upcoming step numbers were white on
  light grey (1.2:1, Tailwind gray-200); they are now muted ink on the surface (7.7:1
  light, 8.2:1 dark).
- lucide-react icons instead of emoji, Inter + Fraunces via next/font,
  desktop nav plus a mobile bottom tab bar, BM | EN switch.
- Shared topic form with example topics, skeleton loading states,
  localised error notices with retry, empty states, focus styles,
  skip link, focus trap in the settings dialog (now portalled to body).
- Quiz: difficulty and length options, per-question feedback with the
  explanation, results with a review list. Flashcards: card grows with
  its text, end-of-deck summary. Roadmap: "Mark as done" moves the
  current step forward.
- A test fails on hard-coded words in JSX text.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@aimanmalib
aimanmalib merged commit 6022681 into main Oct 20, 2026
2 checks passed
@aimanmalib
aimanmalib deleted the feat/ui-bilingual branch October 20, 2026 09:49
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