The Languages Site

Learning Website with Next.js

Chapter 7 ยท The Languages Site

The earlier chapters built the machinery; this one finishes the first real site. The languages site needs a front page that shows each language and its courses in the order a learner meets them, a colour for each language that stays readable, and answer blocks that work. As before, the grouping is checked against the Django project on the real content, and the answer blocks are tested in a real browser.

Run for real, on 20 courses and 4,417 pages
56 tests pass; the front page's data is identical to Django's; the site builds (601 pages, 17 s from cold); a headless Chrome opened and closed an answer block. Keyboard use, a screen reader and other browsers were not tried.

The Front Page: Grouped the Way a Learner Thinks

It is not an alphabetical list of folders. Each language has its courses in the order they are met:

GroupHoldsHow a course is placed
Reading and writingAlphabets and kanaThe folder name contains alphabet, hiragana, katakana or kanji; ordered by that, then course number
Survival <Language>Basic Conversation 1 to 3The folder name contains -basic- and the course number is 3 or less
Everyday <Language>Basic Conversation 4 to 6The same, with a number above 3
CultureCourses about the countryThe folder is under culture/; ordered by name
Other coursesAnything else with numbered chaptersOrdered by name

This is decided by plain functions in a package, @lw/languages-site, so it needs no browser to test. A new course appears on the front page when its folder first has a numbered chapter: there is no list to edit. The real site has 8 groups over 20 courses: French has Survival French; German has Survival and Everyday German; Hungarian has Reading and writing and Survival Hungarian; Japanese has Reading and writing (5 courses), Survival Japanese and Culture (5 courses).

export function classify(course): { group; key } { const name = course.folder.slice(course.folder.lastIndexOf("/") + 1); if (course.folder.startsWith("culture/")) return { group: "Culture", key: [course.name] }; const writing = WRITING.findIndex((word) => name.includes(word)); if (writing >= 0) return { group: "Reading and writing", key: [writing, course.courseNo ?? 0] }; if (name.includes("-basic-")) { const number = course.courseNo ?? 0; return { group: number <= 3 ? "Survival" : "Everyday", key: [number] }; } return { group: "Other courses", key: [course.name] }; }

Is It the Same as Django's?

A script in the Django project writes what its front page holds; the TypeScript version is asked the same question:

LanguageGroupsCoursesLessonsCompared with Django
French113identical
German240identical
Hungarian2412identical
Japanese311300identical
Two planted mistakes: one caught, one not
  • “Basic 3” treated as Everyday (<= 2 instead of <= 3): German and Hungarian came out different. Caught.
  • Sort changed to localeCompare (the browser's language rules instead of code-point order): 0 different on the real content. Not caught, because the five Japanese culture courses happen to sort the same either way. A unit test with chosen awkward names (Banana, Zebra, apple) does catch it (55 passed, 1 failed).
The takeaway: real data proves only the cases it contains. A test with deliberately awkward data covers the rest.

A Colour for Each Language, and Text You Can Read

Each language's section carries two CSS variables, --lang-accent (the border) and --lang-text (the heading text), set inline on the section. French is blue, German gold, Hungarian green and Japanese red, from their flags. One catch: Japanese's red (#d64550) is only 3.86 against the dark surface, and text needs 4.5. So Japanese keeps its true red for the border and uses a lighter red of the same hue (#e8707a) for its heading. A test keeps every language's text colour at 4.5 or more, reads the real surface colour out of tokens.css, and checks that Japanese's accent alone would fail, which is why the second colour exists.

The page's stylesheet is a CSS Module: only this page uses those classes, so by Chapter 5's rule they are not global. A screenshot in headless Chrome shows a coloured left border and name for each language, the group titles in small capitals, and each course with its chapter count.

Course and Chapter Pages

Nothing language-specific was needed. A course is a folder listing headed with the course's name, and a chapter has a breadcrumb and previous and next links, all from Chapter 6. The lessons themselves keep their own accent colour (green Hungarian, gold German, red Japanese, blue French) from their own scoped style, as Chapter 5 found, and their lang marks from Chapter 4.

Answer Blocks

Most language lessons end with a “Try It Yourself” block whose answers are hidden until asked for. In the files they are plain <details> elements with a <summary> (“Show answer”): no script, so nothing to re-run and nothing to break. 196 of the site's 601 pages contain them. That is a claim about a real browser, so it was tested in one:

German Basic Conversation 1, chapter 1 (5 blocks)First block
Before the clickclosed, 24 px high
After one click on “Show answer”open, 60 px high
After a second clickclosed, 24 px high
A mistake in the test itself
What went wrong: my first measurement asked “is the answer's own element visible?” and reported “visible” whether the block was open or closed, because a closed <details> still reports a size for its content. The takeaway: a check that gives the same answer in both states checks nothing, so before trusting one, run it in a state where the answer is known to be “no”. It was replaced with the height of the whole block, which does change.
What was not verified
Keyboard operation of the blocks (browsers open a <details> with Enter or Space on its summary, but that was not tried), a screen reader, other browsers, and the print layout. Only the languages site is complete; the others have a menu but no pages yet.

Hands-On Exercises

Exercise 1

Group the language courses the way a learner meets them (reading and writing, survival, everyday, culture, other), with readable text colours for every language. Prove the result against the Django project on the real content, and show with planted mistakes what the real data does and does not catch.

๐Ÿ“„ View solution
Exercise 2

Build the languages front page as a server component with a CSS Module, a section and colour for each language, and a lessons line only where lessons exist. Build the site, fetch the page and check its sections, groups and colours.

๐Ÿ“„ View solution
Exercise 3

Test in a real browser that a lesson's “Show answer” blocks open and close. Make sure your measurement gives different answers in the open and closed states, and say what you did not test.

๐Ÿ“„ View solution

Chapter 7 Quick Reference

  • Front page: each language (French, German, Hungarian, Japanese), its courses in groups: Reading and writing, Survival, Everyday, Culture, Other courses
  • Group by folder name and course number; sort numbers by number and names by code point (not localeCompare)
  • Identical to Django: 4 sections, 8 groups, 20 courses; a "Basic 3 is Everyday" mistake was caught, a sort mistake was not (a unit test catches it)
  • Two CSS variables per language section: --lang-accent (border) and --lang-text (heading); Japanese's text red is lighter for contrast 4.5
  • The front page's classes are a CSS Module; the data comes from groupCourses at build time, so a new course appears by itself
  • Course and chapter pages are Chapter 6's: a course is a listing headed with its name, a chapter has a breadcrumb and previous/next
  • "Try It Yourself": plain <details>, no script; tested: closed 24 px, open 60 px, closed again
  • A check that cannot tell open from closed checks nothing: run it in a state where the answer is "no"
  • Not tried: keyboard, a screen reader, other browsers, print