Multilingual dining menus for UH Mānoa

2024 · ongoing

This started as a group project in a UH Mānoa software engineering course. After the semester I simplified the original team codebase and still maintain it. The stack is Next.js, TypeScript, and Postgres through Prisma. The live app is manoa-menu.vercel.app. The repository is github.com/manoa-menu/manoa-menu.

The site lists weekly menus for Campus Center, Gateway Café, and Hale Aloha in English, Japanese, Korean, and Chinese. Language and location stay in the browser. Non-English views include a notice that the copy is model-generated.

Menu ingestion

Campus Center does not offer a menu API. The server fetches the dining page, finds this week's PDF from date ranges in Hawaii time, and sends the file to a model with a strict JSON schema for plate lunch and grab-and-go by day. English is stored once per week. Later requests skip the download.

Gateway and Hale Aloha come from a vendor API keyed per day. Nutrition fields are dropped. Closed-day placeholders are dropped. An empty day is not cached, so a later request can pick up a menu if the hall reopens. When a hall publishes special hours, that notice is shown instead of an empty list.

Translation pipeline

English is the source. Unique strings for the week are translated into the selected language, then stored by a hash of the source text. A per-language lock keeps two overlapping requests from running the same translation twice. The model returns JSON of equal length and order. It may add a short gloss only when a native speaker would still not know what the dish is. Japanese also runs a small set of manual substitution rules after the model returns.

async function withLanguageLock(language, work) {
  const owner = crypto.randomUUID();
  await waitForLock(language, owner, 45_000);
  try {
    return await work();
  } finally {
    await releaseLock(language, owner);
  }
}

Native speakers can open an internal review page after presenting a secret token. An edit writes through to the live menu and is reused the next time that string appears. The route is not indexed. A missing token returns 404.

Campus Center food court menu in Japanese.
Campus Center in Japanese. The same week as English, with a language control in the header.

Hours, rate limits, and usage

Open and closed chips use Pacific/Honolulu, not the server's local time. A directions control opens the device maps app at the selected hall.

Menu routes reject a client after 30 requests in a minute. Model calls go through OpenAI or OpenRouter from one config flag. Each call records tokens and estimated USD. An internal usage page is gated by a timing-safe secret and is not indexed. GitHub Actions runs typecheck, ESLint, and unit tests on push.

All projects