feedback5 WAD: docs for language packs and content packs — docs/PACKS.md (the manifest, the two layouts, discovery and the index, what is served, the types and how a later type plugs in), docs/I18N.md §5 "Adding a language" (step by step for a translator) with the fallback chain, placeholder validation and plural categories, CONTRIBUTING / README pointers, DEPLOY §7 (packs/ and packs/index.json in the zips), ARCHITECTURE, docs/history/0.2.0.md §25.16; build-i18n --dict (a community dictionary for a language without an official client); the browser check of a pack file and a pack folder dropped in while the server runs (test/ui/i18n.e2e.test.js); golden unchanged
This commit is contained in:
1 parent
25b2a87b26
commit
1ee39a79b5
11 files changed
+466
-64
No files matched your search
+164
@@ -0,0 +1,164 @@
|
||||
# PACKS.md — content packs
|
||||
|
||||
A **content pack** is add-on content a server picks up from a folder: a manifest plus its files. The owner's decision of
|
||||
2026-10-07 made the language pack the first type of one general mechanism, so that later resource packs (art, audio,
|
||||
fonts) and server-side data packs reuse the same manifest, registry and index instead of each inventing its own. In
|
||||
0.2.0 one type loads, **`lang`** (a language: docs/I18N.md "Adding a language"); `assets` and `data` are planned.
|
||||
|
||||
| File | Role |
|
||||
|---|---|
|
||||
| `shared/packs.js` | the format: the manifest, the pack types (`PACK_TYPES`), app version ranges, the index (browser, server and tools) |
|
||||
| `shared/i18nPacks.js` | the `lang` type: language codes, the language fields, the fallback chain, the script family |
|
||||
| `server/packs.js` | the registry: discovery, validation, the index, which pack files may be served |
|
||||
| `server/http/static.js` | `GET /packs/index.json` (the live index) and `GET /packs/<id>/<file>` |
|
||||
| `public/js/ui/lang.js` | the client of the `lang` entries: the language menu and the loader |
|
||||
| `tools/packs.mjs` | `list`, `index` (the index file for a static host), `check` |
|
||||
| `tools/package.mjs` | ships the committed packs and writes `packs/index.json` into the release zips |
|
||||
|
||||
## 1. Layouts
|
||||
|
||||
Two layouts, read by the same registry and listed alike:
|
||||
|
||||
```
|
||||
packs/<id>/pack.json + its files a folder pack — any type; the manifest names its files
|
||||
public/i18n/<code>.json a single-file language pack — its manifest is the file's `_meta` block
|
||||
data/i18n/<code>.json (+ its game texts, when present)
|
||||
```
|
||||
|
||||
The single file is the quickest way for a translator (one file, the format English has always had, and how English
|
||||
ships); the folder is the layout every pack type shares — a pack distributed as one folder with its own readme and
|
||||
licence, and the only layout a resource or data pack will have. A language can come either way; the menu does not tell
|
||||
them apart.
|
||||
|
||||
The id of a folder pack is its folder name; the id of a single-file language pack is its language code. Ids are
|
||||
letters, digits, `.`, `_` and `-` (at most 64), unique across types and without regard to case (Windows and macOS
|
||||
would see one folder).
|
||||
|
||||
## 2. The manifest
|
||||
|
||||
`packs/<id>/pack.json`, or the `_meta` block of `public/i18n/<code>.json` (the same fields):
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "lang",
|
||||
"lang": "ja",
|
||||
"name": "日本語",
|
||||
"englishName": "Japanese",
|
||||
"version": "1.0.0",
|
||||
"app": ">=0.2.0",
|
||||
"authors": ["…"],
|
||||
"credits": "…",
|
||||
"license": "CC-BY-4.0",
|
||||
"fallback": ["en"],
|
||||
"files": { "ui": "ui.json", "data": "data.json" }
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Meaning |
|
||||
|---|---|
|
||||
| `id` | optional: the folder or file name decides; a manifest that names another id gets a warning |
|
||||
| `type` | `lang` (a single-file language pack may leave it out); planned: `assets`, `data` |
|
||||
| `version` | the pack's own version |
|
||||
| `app` | the app versions it is made for, a range: `>=0.2.0`, `>=0.2.0 <0.3.0`, `0.2.x`, `^0.2.0`, `~0.2.1`, `*`, alternatives with `||`. A pre-release (0.2.0-dev) counts as its release. Outside the range a language pack still loads — what it lacks falls back — and is flagged `compatible: false` (the server log, `tools/packs.mjs list`); a later type decides what an incompatible pack means for it |
|
||||
| `name`, `englishName` | the name in its own language (a language pack: the language's own name, shown in the menu) and in English (the menu's tooltip) |
|
||||
| `authors`, `credits`, `license` | who made it; free text (`license`: an SPDX id or text) |
|
||||
| `files` | a folder pack: role → path inside the folder. Each type lists its roles and their extensions (`lang`: `ui` required, `data` optional, both `.json`) |
|
||||
| the type's fields | `lang`: `lang`, `base`, `fallback`, `complete`, `numberUnits` (docs/I18N.md) |
|
||||
|
||||
A **problem** keeps a pack from loading (unknown type, no `lang`, a file missing or outside the folder, broken JSON, an
|
||||
id or a language already taken — the language folders' files come first, then the folders by id); a **warning** does
|
||||
not (an id that differs, an app range this version is outside of, an unknown file role). Both are printed when the
|
||||
server starts or notices a change, and by `node tools/packs.mjs list`.
|
||||
|
||||
## 3. Discovery and the index
|
||||
|
||||
The server's registry (`server/packs.js`) scans `public/i18n/`, `data/i18n/` and `packs/` when it starts and again
|
||||
whenever something there changes: on a request for the index or a pack file it compares the names, sizes and times of
|
||||
the files it reads (at most once a second, a few `stat` calls) and scans again if they differ. A pack dropped into a
|
||||
folder shows on the next page load — no restart, no build step; a removed one leaves (a player whose stored language is
|
||||
gone gets Chinese).
|
||||
|
||||
`GET /packs/index.json` (never cached) answers the packs of supported types:
|
||||
|
||||
```json
|
||||
{ "version": 1, "app": "0.2.0", "packs": [
|
||||
{ "id": "en", "type": "lang", "name": "English", "englishName": "English", "version": "0.2.0", "app": ">=0.2.0",
|
||||
"compatible": true, "authors": ["…"], "lang": "en", "fallback": [], "complete": true, "strings": 1017,
|
||||
"files": { "ui": "/i18n/en.json", "data": "/data/i18n/en.json" } } ] }
|
||||
```
|
||||
|
||||
`files` holds URLs on this server, so a client never builds a path itself. A client reads the entries of its type
|
||||
(`readPackIndex(json, 'lang')`: an entry of another type, with a bad id or a URL that is not a path on this server is
|
||||
dropped).
|
||||
|
||||
**A static host** cannot list folders: `node tools/packs.mjs index` writes the same body to `packs/index.json`
|
||||
(git-ignored), and `tools/package.mjs` writes it into the release zips. The live server answers `/packs/index.json`
|
||||
itself and ignores that file. Serving the game from plain files needs the server's URL layout: `public/` at `/`,
|
||||
`data/` at `/data/`, `shared/` at `/shared/`, `server/sim/` at `/sim/` and `packs/` at `/packs/` (the game itself still
|
||||
needs the Node server for its rooms; DEPLOY.md).
|
||||
|
||||
**What is served.** `GET /packs/<id>/<file>` answers only a file that the manifest of a loaded folder pack names under a
|
||||
role of its type, with that role's extension, inside the folder once links are resolved — `pack.json`, a readme, an
|
||||
HTML or script file in the folder are never served; a pack of a planned type serves nothing. A single-file language
|
||||
pack's files are the language folders' own (`/i18n/<code>.json`, `/data/i18n/<code>.json`).
|
||||
|
||||
## 4. The types
|
||||
|
||||
| Type | Status | Where it applies | Loader |
|
||||
|---|---|---|---|
|
||||
| `lang` | supported (0.2.0) | the client: UI strings, game texts | `public/js/ui/lang.js` (the menu, the strings: `shared/i18n.js`; the game texts: `public/js/data.js`) |
|
||||
| `assets` | planned | the client: art, audio, font replacements | — |
|
||||
| `data` | planned | the server: data patches applied at start | — |
|
||||
|
||||
A pack of a planned type is parsed and listed as not supported ("type "assets" is planned …, not loaded by this
|
||||
version"); it is never served and never changes anything.
|
||||
|
||||
### Adding a pack type
|
||||
|
||||
1. **The type** — its entry in `PACK_TYPES` (`shared/packs.js`): `status`, `summary`, `live` (whether a running server
|
||||
may take changes without a restart) and `files` (the roles a folder pack names, with the extensions that may be
|
||||
served for each). Its own manifest fields get a normalizer beside `langFields` and a branch in `normalizeManifest`;
|
||||
its index fields a branch in `packIndexEntry`.
|
||||
2. **The loader** — reads the registry by type: `createPackRegistry(…).list('assets')` on the server, or the index
|
||||
entries of the type in the browser (`readPackIndex(json, 'assets')`), the way `ui/lang.js` reads `lang`. A
|
||||
server-side type (`data`) loads once at start (`live: false`): the simulation's code and data are fixed for the life
|
||||
of the process, and a battle must run the same data on the server and in every browser.
|
||||
3. **The checks** — what `tools/packs.mjs check` and the tests verify for the type (a data pack: the same validation
|
||||
the data builder applies; a resource pack: the files exist and are of the allowed kinds), and its documentation
|
||||
here.
|
||||
|
||||
Things later types must keep (the maintainers' study of mods, 2026-10-07):
|
||||
|
||||
- **Server-side only for gameplay.** A data pack is installed on the server, so every player of a room uses the same
|
||||
content and the server can still check or recompute a battle. A player never installs gameplay content of their own.
|
||||
- **A content hash in the handshake** before data packs ship (`welcome`, `/healthz`, the BattleSpec), so a client and
|
||||
a server can tell they run the same content, and a "modded" mark in the UI and in bug reports.
|
||||
- **No third-party code.** The manifest and its files are data (JSON, images, audio, fonts); a pack type that runs code
|
||||
is a separate decision of the owner's.
|
||||
- **The golden results stay on the original content**; a pack never changes them.
|
||||
|
||||
## 5. Tools
|
||||
|
||||
```
|
||||
node tools/packs.mjs list [--json] the packs by type, what was skipped and why, the warnings
|
||||
node tools/packs.mjs index [--out <file>] write packs/index.json (the static index)
|
||||
node tools/packs.mjs check [--strict] list; --strict exits 1 when a pack is skipped or warned about
|
||||
node tools/i18n.mjs template <code> a new language pack (docs/I18N.md)
|
||||
node tools/i18n.mjs check <code> | --all a language pack's strings: coverage, placeholders
|
||||
```
|
||||
|
||||
`--root <dir>` points `tools/packs.mjs` at another checkout or an unpacked release.
|
||||
|
||||
## 6. Shipping
|
||||
|
||||
`tools/package.mjs` ships the committed packs (`public/i18n/`, `data/i18n/`, `packs/` — tracked files only: a pack
|
||||
installed on this machine and not committed stays out) and writes `packs/index.json` into the stage from the packs
|
||||
that ship. `test/package.test.js` checks both layouts and that a local pack stays out.
|
||||
|
||||
## Tests
|
||||
|
||||
`test/i18n-packs.test.js` (discovery of both layouts, broken / planned / duplicate packs, a pack dropped in or removed
|
||||
while the server runs, the file allowlist, the manifest and app ranges, the fallback chain, placeholders, plurals,
|
||||
the per-text game-text fallback, the client loader and menu, the tools, build-i18n `--lang` / `--dict`),
|
||||
`test/package.test.js`, `test/ui/i18n.e2e.test.js`
|
||||
(`SP_E2E=1`: a pack file and a pack folder dropped in while the server runs show in the menu and switch).
|
||||
Reference in new issue
Block a user