Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 22 additions & 6 deletions .agents/code.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ src/cheevos/
ui/
pyui/ bridge: host (what a session borrows from PyUI: textures,
messages, Spruce's settings), views (lists, grids, carousel,
popups), primitives (drawing, input), text (fitting, glyph
popups), primitives (drawing, input), text (fitting, wrapping, glyph
fallbacks), status_bar,
bar_layout, glyphs, generated (PNG swatches, button glyphs),
award_images (static awards), row_bars, bar_colors, image_frames
Expand All @@ -87,7 +87,11 @@ src/cheevos/
settings, setup, status (bottom-bar sync status), rows, common
context.py what screens share (AppContext)
media.py image resolver with outline-icon fallbacks
strings.py every user-facing string (ready for PyUI Language later)
strings.py every user-facing string, in English
language.py the session's language: a translation from res/lang, checked against
strings, over the English values
plurals.py plural rules for the languages PyUI offers
format.py numbers, dates, relative times and counts, in the session's language
platform/
paths.py every path, from CHEEVOS_SDCARD_ROOT (default /mnt/SDCARD)
identify_rom.py the identification helper's entry point (identify-rom.sh runs it)
Expand All @@ -97,15 +101,17 @@ src/cheevos/
from the proxy)
desktop/ dev only: runner CLI on PyUI's DESKTOP device (pyui_setup brings
PyUI up), fixture/live environments, drills (showcase_awards: the
showcase's event and site badges)
showcase's event and site badges), translations (the English
template and the pseudo-language)
res/icons/ original outlines: 24/48 px status, 96/144 px lists and fallbacks
res/awards/ pre-rendered awards: dark/light and black/white contrast variants
res/lang/ translations, <Language>.json as PyUI names its language files
app/ packaging: config.json, identify-rom.sh, cheevos.png
assets/ icon sources (SVG)
tests/ unit and screen tests, fixtures/
scripts/ check_conventions, record_fixtures, fetch_dev_media, fetch_pyui.sh,
build_package, release_notes, render_icons, make_synthetic_cache,
doc_screens, device.sh (with device_session: make launch's
check_languages, doc_screens, device.sh (with device_session: make launch's
dev-only session), fetch_proxy.sh, check_proxy_compat (with
proxy_native: ELF exports, a simulated native hasher)
docs/ the user guide, published as the GitHub wiki (images/ from doc_screens)
Expand Down Expand Up @@ -159,6 +165,9 @@ knowing about:
3. **Layering**: checks `import` and `from … import` of PyUI top-level modules (`display`,
`views`, `themes`, `controller`, `devices`, `menus`, `utils`, `apps`, `games`, `audio`) and of
`cheevos.ui` from inside `cheevos.core`.
4. **Strings read at import** (CV007): `strings.NAME` in a module constant, default value,
decorator or class attribute, or `from cheevos.ui.strings import`, would stay English
([product.md](product.md), "Languages").

Tooling gotchas:
- **Formatter vs. regex edits:** `ruff format` re-wraps long calls, so a regex edit of
Expand Down Expand Up @@ -229,8 +238,15 @@ Tooling gotchas:
- Every SD-card path comes from `cheevos.platform.paths`, rooted at `CHEEVOS_SDCARD_ROOT`
(default `/mnt/SDCARD`).
- Never hard-code `/mnt/SDCARD` anywhere else.
- **User-facing strings** live in `cheevos/ui/strings.py`, keyed, so they can map to PyUI's
`Language.label()` later.
- **User-facing strings** live in `cheevos/ui/strings.py`, in English, and are translated per
session ([product.md](product.md), "Languages"):
- Read `strings.NAME` when the text is shown, never at import (CV007).
- A string that counts something by `{count}` is a `Plural`, formatted with `fmt.count()`
(also for English's "1 event" / "5 events").
- Numbers, decimals, percentages, sizes and dates go through `ui/format.py`, never f-string
format specs or `strftime`.
- Renaming a string, or changing its fields, leaves translations behind: `make languages`
shows what they lose.

## Tests
- **Layout**: `tests/unit/` mirrors `src/cheevos/core/`, `tests/screens/` holds headless render
Expand Down
43 changes: 43 additions & 0 deletions .agents/product.md
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,49 @@ so the bridge shows "[START] Done [B] Delete [L1] Shift [R1] Caps" in the bottom
- A list title that doesn't fit ends with "..." before the value on its right; the selected
one scrolls after a second so it can be read (PyUI's behaviour, as in Spruce's own lists).

## Languages
- **Follows Spruce.** Cheevos shows itself in Spruce's UI language (PyUI's setting,
`PyUiConfig.get_language()`), read when a session starts (`ui/language.py`). It has no
language setting of its own.
- **Translations ship with Cheevos**, one per language in `src/cheevos/res/lang/<Language>.json`,
named as PyUI names its language files (`Catalan.json`, `Chinese (S).json`), as Spruce's file
manager names its own (SpruceOS #1962). Not in PyUI's `lang/` files: Cheevos' keys and fields
change with its releases, so the translations are versioned and checked with the app (agreed
2026-11-01, abalakh/cheevos#16). Contributors' workflow: CONTRIBUTING.md, "Translating".
- **A file** maps the names in `ui/strings.py` to values of the same shape:
- text for text;
- for a `Plural`, text or `{"one": ..., "few": ..., "many": ..., "other": ...}` (`other`
required);
- for a dict of labels (filters, sorts, views), any of its keys;
- for a tuple (`MONTHS`, `SIZE_UNITS`, `ABOUT_TEXT`), a list of the same length.
Keys starting with `_` are notes for translators; `""` means not translated yet.
- **Anything missing stays English**, and so does a value that wouldn't format like the English
one: an unknown or missing field, another format spec than English's, a stray brace. Each is
logged as a warning, and `make languages` lists them with what isn't translated yet. A plural
form other than `other` may leave fields out ("ahir", no `{count}`).
- **Plurals**: CLDR's categories (`one`, `few`, `many`, `other`), with a rule per language PyUI
has (`ui/plurals.py`); other languages get English's. Every string that counts something by
`{count}` is a `Plural`, and `format.count()` picks the form and formats the count with the
language's thousands separator.
- **Dates and numbers** come from strings too: `MONTHS`, `DATE`, `DATETIME` (24-hour time),
`THOUSANDS_SEPARATOR`, `DECIMAL_SEPARATOR`, `PERCENT`, `SIZE`, `SIZE_UNITS`. Never `strftime`'s
`%b`: the devices run the C locale, so it's always English.
- **Read strings when the text is shown**, never at import: the translation is applied after the
modules load. `scripts/check_conventions.py` (CV007) rejects module constants, defaults and
class attributes that read `strings.NAME`, and `from cheevos.ui.strings import`.
- **Stays English**: what RetroAchievements sends (game, achievement, event and console names,
descriptions, rich presence), the logs, and button names in the bottom bar's text fallback
("Start: Done").
- **PyUI's own text** on Cheevos' screens (the theme's "A Okay B Back" hints, the on-screen
keyboard, "The app stopped after an error") follows PyUI's language files.
- **Scripts and fonts**: the SPRUCE theme's `nunwen.ttf`, also PyUI's fallback font, has Latin,
Cyrillic, Greek, Chinese, Japanese, Korean and Vietnamese, but not Thai, Khmer, Lao or
Devanagari (checked 2026-11-01). Other themes may lack accents (shown as the plain letter) or
Chinese and Japanese (boxes, as in PyUI's own menus: PyUI never uses its fallback font). Text
without spaces breaks where the line ends (`text.wrap`).
- **Checking a translation**: `make screens UI_LANGUAGE=<Language>`; `UI_LANGUAGE=Pseudo` shows
every string accented, longer and bracketed (TESTING.md, "Languages").

## Offline and error behaviour

| Situation | Behaviour |
Expand Down
12 changes: 10 additions & 2 deletions .agents/pyui.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,10 @@ views, without patching PyUI.
an error" with the log's path. PyUI is already up and owns its device, configs, theme, display,
controller, view state (`PyUiState`), logger and signal handlers; Cheevos sets none of them
up. Spruce's environment (`PLATFORM`, `SSL_CERT_FILE`) is PyUI's. PyUI's configs and Spruce's
are read, never written, except the API key copy below.
are read, never written, except the API key copy below. `session` first applies Spruce's UI
language (`host.language()`, PyUI's `PyUiConfig.get_language()`); PyUI has already loaded its
own `Language`, and Cheevos doesn't call `Language.init()` ([product.md](product.md),
"Languages").
- **Leave PyUI as we found it.** `session` undoes everything it puts into PyUI, in reverse and
even after an error (an `ExitStack`): the texture budget (`host.enter`/`leave`); the bottom-
and top-bar hooks (`status_bar.installed`, `title_bar.installed`, inside `app.run`); the
Expand Down Expand Up @@ -99,6 +102,9 @@ theme, display, controller.
`PyUiLogger._logger` instead (`install_logger`).
- **Configs:** `UserConfig.FILEPATH` is hard-coded to the SD card, so the runner points it at
the fake card; PyUI's view state goes to `Saves/cheevos/pyui-state.json` there.
- **Language:** `--lang <Language>` writes `"language"` into the runner's PyUI config, so PyUI's
own text and the app both follow it, as on a device. `--lang Pseudo` leaves PyUI in English
(it has no such file) and hands the app a generated translation instead.

## View gotchas
- **Timeouts:** `view.get_selection()` never returns `None`. On a timeout or cursor move it
Expand Down Expand Up @@ -264,7 +270,9 @@ theme, display, controller.
- **Image measurement:** `Display.get_image_dimensions` loads the file every call. Cache widths.
- **Fonts:** theme fonts vary. Pico-8 has no "·" or "…" and no accented letters, and no theme has
emoji. Every string goes through `text.displayable(value, role)`, which checks glyphs with
`TTF_GlyphIsProvided32` and falls back to ASCII (accents: the NFKD base letter).
`TTF_GlyphIsProvided32` and falls back to ASCII (accents: the NFKD base letter). Scripts the
font lacks entirely (Chinese in Pico-8) draw as boxes: nothing uses PyUI's fallback font
(`allow_fallback_font_for_missing_glyphs`), PyUI's own menus included.
- **Image scaling:** PyUI scales images with linear filtering, which blurs pixel art.
`primitives.sharp_scaled` enlarges by the next whole factor above the fit with
nearest-neighbour (BMP in the RAM scratch), then PyUI shrinks it into the box: "sharp
Expand Down
5 changes: 5 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ Instructions for coding agents. Claude Code reads this file itself (v2.1.277+),
X=A, Z=B, S=X, A=Y, Q/W=L1/R1, Enter=Start, RShift=Select, M=Menu, F8=screenshot, Esc=quit.
- Drills: `CHEEVOS_SIMULATE=offline|clock|auth|empty|proxy|showcase|awards|setup make run`.
- `make screens`: headless PNGs in `build/screens/<WxH>/`. Read them to check UI changes.
`UI_LANGUAGE=Catalan` (a translation in `src/cheevos/res/lang/`) or `UI_LANGUAGE=Pseudo`
renders them in another language, into `build/screens/<Language>/<WxH>/`; `make run` takes it
too.
- `make languages`: check the translations; writes the English template to
`build/lang/English.json`.
- Ad hoc: `uv run python -m cheevos.platform.desktop --headless --res 640x480 --script "shot:a,down,shot:b"`
(tokens: buttons, `shot:<name>`, `wait:N` idle ticks of ~1/12 s).
- `make doc-screens`: regenerate the wiki screenshots after UI changes.
Expand Down
24 changes: 24 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,30 @@ and it's also the best reference for people changing the code.

CI and the maintainer's publishing workflow are in [RELEASING.md](RELEASING.md).

## Translating
Cheevos shows itself in the language chosen in Spruce's settings, when it has a translation for
it. Translations live in this repository, not in Spruce: SpruceOS copies each Cheevos release
whole, so a change to `App/Cheevos/` in Spruce is lost at the next one.

1. Run `make languages`. It writes every string in English to `build/lang/English.json`.
2. Copy it to `src/cheevos/res/lang/<Language>.json`, named exactly as Spruce's own file for the
language in `App/PyUI/lang/` (`Catalan.json`, `Chinese (S).json`), and translate the values.
Keep the names on the left as they are.
- Keep every `{field}`: the app fills them in. `{day:02d}` may become `{day}`.
- A text with `{count}` that counts something takes plural forms:
`{"one": "{count} punt", "other": "{count} punts"}`. Use the forms your language needs
(`one`, `few`, `many`, `other`; `other` is required). The rules for each language are in
`src/cheevos/ui/plurals.py`.
- Lists keep their length: 12 `MONTHS`, 4 `SIZE_UNITS`.
- Leave a value as `""`, or out, to keep it in English for now. Keys starting with `_` are
notes and are ignored, e.g. `"_translators": "Your name"`.
3. Run `make languages` again until it reports nothing left out, and `make check`.
4. Run `make screens UI_LANGUAGE=<Language>` and look through `build/screens/<Language>/`: text
that's cut short or crowded needs a shorter wording. Attach a few screenshots to the pull
request.

Game, achievement and console names come from RetroAchievements and stay in English.

## Pull requests
- One topic per pull request. Say what changed and why, and add screenshots for UI changes.
If you tried it on a device, say which one.
Expand Down
18 changes: 12 additions & 6 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,15 @@ RES ?= 640x480
SCALE ?= 2
SCREEN_RESOLUTIONS ?= 640x480 752x560 1280x720
# Walk-through of the real app on recorded fixtures (desktop fixture mode).
# The app's language for run and screens: a translation in src/cheevos/res/lang, or Pseudo.
UI_LANGUAGE ?=
LANG_OPTION = $(if $(UI_LANGUAGE),--lang "$(UI_LANGUAGE)")
SCREEN_SCRIPT ?= shot:home,a,shot:profile,b,down,a,shot:games,select,shot:games_details,select,down,a,shot:game,right,a,shot:achievement,a,shot:screenshot,b,b,y,shot:options,b,b,b,down,a,shot:recent,b,down,a,shot:awards,a,down,a,shot:settings
SHELL_SCRIPTS := $(shell find app scripts -name '*.sh' 2>/dev/null)

.DEFAULT_GOAL := help
.PHONY: help sync pyui proxy proxy-check fmt lint conventions shellcheck typecheck test check run screens \
doc-screens wiki package deploy launch stop logs shot clean
languages doc-screens wiki package deploy launch stop logs shot clean

help: ## Show this help
@grep -E '^[a-z-]+:.*## ' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*## "} {printf " %-12s %s\n", $$1, $$2}'
Expand Down Expand Up @@ -54,18 +57,21 @@ test: ## Run the test suite with coverage

check: lint typecheck test ## Everything CI runs

run: ## Desktop window: make run [RES=752x560] [SCALE=1]
$(RUN) python -m cheevos.platform.desktop --res $(RES) --scale $(SCALE)
run: ## Desktop window: make run [RES=752x560] [SCALE=1] [UI_LANGUAGE=Catalan]
$(RUN) python -m cheevos.platform.desktop --res $(RES) --scale $(SCALE) $(LANG_OPTION)

screens: ## Headless PNGs of the walk-through into build/screens/<WxH>/
screens: ## Walk-through PNGs in build/screens/[<UI_LANGUAGE>/]<WxH>/ (UI_LANGUAGE=Catalan or Pseudo)
@cards=$$(mktemp -d) || exit 1; \
trap 'rm -rf "$$cards"' 0; \
trap 'exit 1' 1 2 15; \
for res in $(SCREEN_RESOLUTIONS); do \
CHEEVOS_SDCARD_ROOT="$$cards/$$res" $(RUN) python -m cheevos.platform.desktop \
--headless --res $$res --script "$(SCREEN_SCRIPT)" || exit 1; \
--headless --res $$res --script "$(SCREEN_SCRIPT)" $(LANG_OPTION) || exit 1; \
done
@echo "Screens written to build/screens/"
@echo "Screens written to build/screens/$(if $(UI_LANGUAGE),$(UI_LANGUAGE)/)"

languages: ## Check translations; write the English template to build/lang/English.json
$(RUN) python scripts/check_languages.py --template build/lang/English.json

doc-screens: ## Render the wiki screenshots into docs/images (showcase drill)
$(RUN) python scripts/doc_screens.py
Expand Down
18 changes: 18 additions & 0 deletions TESTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,24 @@ different times come out the same.
drill. Themes found in `dev/themes/` (MINIMAL, Pico-8) are included. The clock always shows
10:30, so an image changes only when its screen does.

## Languages
The app follows Spruce's UI language where it has a translation in `src/cheevos/res/lang/`
([.agents/product.md](.agents/product.md), "Languages"). To see it in one:

- `make screens UI_LANGUAGE=Catalan` renders the walk-through in Catalan into
`build/screens/Catalan/<WxH>/`, and `make run UI_LANGUAGE=Catalan` opens the window in it. The
runner sets PyUI's language too, so PyUI's own text (the theme's button hints, the keyboard)
follows PyUI's `lang/Catalan.json` from `.spruceos/`. The log names each value the app left out
and why. Quote names with spaces: `UI_LANGUAGE="Chinese (S)"`.
- `UI_LANGUAGE=Pseudo` uses a generated translation: every string accented, about 40% longer
and in brackets, with Catalan's number separators. Text without brackets isn't translatable;
a missing `]` means the text was cut; crowded rows show where a longer language won't fit.
PyUI's own text stays English.
- `make languages` checks every translation: values the app would leave out, and strings not
translated yet. It also writes every string in English to `build/lang/English.json`, the
starting point for a new language.
- `--lang <Language>` does the same for the runner directly; `--out` still chooses the folder.

## Icon assets

The app and UI icons are original SVGs in `assets/icons/` (design rules in its `README.md`).
Expand Down
6 changes: 6 additions & 0 deletions docs/Themes-and-Devices.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@ Themes with their own button hints in the bottom bar ("A Okay B Back") keep them
its hints after them. Theme fonts don't have emoji, so emoji in rich presence texts are left out.
Some pixel fonts lack accented letters; Cheevos then shows the plain letter ("Pokemon").

## Language
Cheevos follows the language chosen in Spruce's settings, when it has a translation for it.
Otherwise it's in English, and so is any text a translation doesn't cover yet. Game,
achievement and console names, descriptions and rich presence come from RetroAchievements in
English. Translations are welcome: see "Translating" in the project's CONTRIBUTING.md.

## Resolutions
Every resolution Spruce's default theme supports is laid out properly: 640×480, 720×480,
752×560, 720×720, 960×720, 1024×768, 1280×720 and 480×800. Icons are drawn at whole-pixel sizes
Expand Down
Loading
Loading