# 0021. Languages and Chinese printing

- Status: Accepted
- Date: 2026-10-28

## Context

Malaysian shops work in English, Malay and Chinese, often all three in one shop: a cashier
reads Malay, the kitchen reads Chinese, and customers want receipts they can read. M8 adds
Malay (`ms`) and Simplified Chinese (`zh-Hans`) to the POS app, the back office and the printouts.
Each reader needs their own language, and switching must not need a restart.

Thermal printers mostly print only Latin text in their built-in fonts. Models made for the Chinese
market also have a Chinese font (GB2312/GBK, read in their "Chinese mode"), but many printers sold
in Malaysia don't. Printing Chinese must work on both, without native code that can only be
checked on a device.

## Decision

**Who chooses which language.**
- **Device screens:** each POS device chooses its own screen language. It is stored on the
  device, offered on the setup screen, the PIN pad and in Settings → Language, and starts from
  the system language.
- **Back office:** each browser chooses its own language. It is stored in the browser and starts
  from the browser's language.
- **Shop settings:** the shop sets a receipt language, an optional second receipt language, and
  a kitchen ticket language. The kitchen ticket language defaults to the receipt language.
  These settings sync like the other shop settings.
- **Z-reports and test pages:** these are for staff, so they print in the device's screen
  language.

**Switching without a restart.**
- **POS app:** the provider holds the translator, so every screen re-renders when it changes.
- **Back office:** the router remounts with the new language (the data cache stays), and `<html
  lang>` follows.
- **Hub errors:** the hub answers in the language a request asks for (`Accept-Language`). The back
  office and the app send theirs, so errors appear in the reader's language. Handlers send catalog
  keys (`fail("server.…")`), and one hook turns them into words.

**Two languages on a receipt.** With a second language, labels print in both ("Total / Jumlah").
A label followed by one value prints in both, with the value once ("Receipt / Resit K1-0042",
"Served by / Dilayan oleh Aminah"; changed 2026-10-28, after the third Mac pass found those
lines in one language). Messages with several values (tax lines) print in the first language
only, so numbers aren't printed twice. Lines that grow too long wrap. Item names are the shop's
own data and print as entered.

**Catalogs.**
- **Layout:** every language has the same keys, split into the POS app, printing, the back
  office and the hub's messages.
- **Tests:** they check that each language has every key with the same placeholders.
- **No hard-coded text:** a test scans the POS app's and back office's components for text
  written into JSX or into props people read (`label`, `title`, `placeholder`, `aria-label`…).

**Chinese printing.** Each printer chooses one of two modes:
- **Image (the default):** works on every printer that prints images (`GS v 0`), which is all
  the printers we target. A line containing Chinese is drawn as a 1-bit image at the printer's
  own cell size (12 × 24 dots, wide characters 24 × 24) and sent like a QR image, so columns line
  up with lines the printer draws itself. The glyphs are GNU Unifont's 16-dot bitmaps (SIL OFL
  1.1), scaled 1.5× with smoothing.
- **Printer's own font:** for printers with a Chinese font. The line is sent in Chinese mode
  (`FS &`) with GBK bytes, then the job leaves Chinese mode again (`FS .`).

**What the font covers.** GBK: simplified and traditional Chinese, which includes GB2312 and is
read by GB18030 printers. GB2312 alone would miss characters common on Malaysian menus, such as
粿 in 炒粿条 and 叻 in 叻沙. The glyphs and the GBK table are packed into one generated module of
about 580 KB (deflated, base64). It is unpacked into flat arrays the first time Chinese is
printed.

## Consequences

- **Printing:**
  - Every printer can print Chinese out of the box, with no native code and no new permissions.
  - Image lines print a little slower than text and look pixelated next to the printer's own
    font. Shops with a Chinese printer can switch that printer to its own font.
  - Characters outside GBK print as "？". Emoji are among them.
  - Unifont's Latin glyphs, used in image lines, don't match the printer's own font exactly.
- **Size:**
  - The POS app's JavaScript grows by about 580 KB for the font.
  - The back office grows by about 70 KB for the extra languages, now one 510 KB file (150 KB
    gzipped).
- **Translations:**
  - Plugin labels already carry `en`, `ms` and `zh-Hans` (ADR 0018). They now follow the
    screen's language on screens and the receipt language on receipts.
  - Sample menu items, staff names and other shop data are not translated.
  - Adding a language means a catalog, a tag in `LOCALES`, and a review of receipt widths
    (labels must fit a 32-column line).
- **Hub API:** catalog keys for errors are part of how the hub talks. Plugins still send their
  own words.
