feedback5 WAD: language packs are the first type of content packs — a language is a file in a language folder (public/i18n/<code>.json, optional data/i18n/<code>.json) or a pack folder (packs/<id>/pack.json); the server lists them live at /packs/index.json (server/packs.js), the menu shows every pack, strings fall back pack → base → declared fallback (English) → Chinese, broken placeholders are skipped, plurals follow Intl.PluralRules (English unchanged), English carries a _meta manifest like any pack; tools: i18n.mjs template / check <code>, packs.mjs list / index, build-i18n --lang ja|ko|zh-TW (the official JP / KR / TW clients); the release zip ships packs/ and a generated packs/index.json; golden unchanged
This commit is contained in:
1 parent
f1df0d359d
commit
25b2a87b26
22 files changed
+2222
-241
No files matched your search
@@ -4,8 +4,8 @@
|
||||
// * TRUST_PROXY ('auto' default: honour CF-Connecting-IP / X-Real-IP / X-Forwarded-For only from loopback/private
|
||||
// peers such as a local cloudflared; '1' always; '0' never) → net.js trustProxy;
|
||||
// * DEBUG → the console logger's debug level;
|
||||
// * the served directories (public/, data/, shared/ of this repository unless the options name others), and which
|
||||
// startServer() options are handed on to net.js Network and lobby.js Lobby.
|
||||
// * the served directories (public/, data/, shared/ and the content packs' packs/ of this repository unless the
|
||||
// options name others), and which startServer() options are handed on to net.js Network and lobby.js Lobby.
|
||||
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
@@ -34,14 +34,15 @@ export function listenAddress(opts) {
|
||||
}
|
||||
|
||||
/**
|
||||
* The directories the static server reads.
|
||||
* @param {{ publicDir?: string, dataDir?: string, sharedDir?: string }} opts
|
||||
* The directories the static server reads (packsDir: the pack folders, server/packs.js).
|
||||
* @param {{ publicDir?: string, dataDir?: string, sharedDir?: string, packsDir?: string }} opts
|
||||
*/
|
||||
export function serveDirs(opts) {
|
||||
return {
|
||||
publicDir: opts.publicDir || path.join(ROOT, 'public'),
|
||||
dataDir: opts.dataDir || path.join(ROOT, 'data'),
|
||||
sharedDir: opts.sharedDir || path.join(ROOT, 'shared'),
|
||||
packsDir: opts.packsDir || path.join(ROOT, 'packs'),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
+21
-3
@@ -12,6 +12,9 @@
|
||||
// /media/bgm/act1 → public/assets/audio/bgm/act1.mp3 — the same audio files, addressed
|
||||
// **without** an extension so download managers (IDM / 迅雷 …) stop popping a
|
||||
// "下载文件信息" dialog for every BGM track (media.js, shared/media.js, public/js/media.js)
|
||||
// /packs/index.json → the content packs of this server (server/packs.js: the language packs of
|
||||
// public/i18n/ and the pack folders of packs/, re-read when they change; never cached)
|
||||
// /packs/<id>/<file> → a file of a pack folder — only one its manifest names (server/packs.js servable)
|
||||
//
|
||||
// Traversal & dotfile protection, a directory without its trailing slash → 301, 404 page; an absent
|
||||
// data/local-assets.json is answered with an empty manifest. Files go out through files.js serveFile.
|
||||
@@ -20,9 +23,11 @@ import fsp from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import { MEDIA_PREFIX } from '../../shared/media.js';
|
||||
import { ROOT, noopLog } from './config.js';
|
||||
import { sendError } from './common.js';
|
||||
import { sendError, sendJson } from './common.js';
|
||||
import { MIME, GzipCache, isNotModified, serveFile } from './files.js';
|
||||
import { serveMedia } from './media.js';
|
||||
import { createPackRegistry } from '../packs.js';
|
||||
import { PACKS_URL, PACK_INDEX_FILE } from '../../shared/packs.js';
|
||||
|
||||
/** Browser stand-in of server/data.js, served at /data.js (see the header). Served byte for byte: its first line names the entry, server/index.js. */
|
||||
export const DATA_SHIM_JS = `// Generated by server/index.js — browser stand-in for server/data.js (DESIGN §14 client-side combat).
|
||||
@@ -45,10 +50,13 @@ const EMPTY_LOCAL_ART = Buffer.from(JSON.stringify({ version: 1, source: 'none',
|
||||
|
||||
/**
|
||||
* Create the static request handler.
|
||||
* @param {{ publicDir: string, dataDir: string, sharedDir: string, simDir?: string, log?: object }} dirs
|
||||
* @param {{ publicDir: string, dataDir: string, sharedDir: string, simDir?: string, packsDir?: string,
|
||||
* packs?: ReturnType<typeof createPackRegistry>, log?: object }} dirs
|
||||
* packs: the server's pack registry (default: one over publicDir, dataDir and packsDir — ROOT/packs)
|
||||
* @returns {(req: import('node:http').IncomingMessage, res: import('node:http').ServerResponse, rawPath: string, query: string) => Promise<void>}
|
||||
*/
|
||||
export function createStaticHandler({ publicDir, dataDir, sharedDir, simDir = path.join(ROOT, 'server', 'sim'), log = noopLog }) {
|
||||
export function createStaticHandler({ publicDir, dataDir, sharedDir, simDir = path.join(ROOT, 'server', 'sim'), packsDir = path.join(ROOT, 'packs'), packs = null, log = noopLog }) {
|
||||
const registry = packs || createPackRegistry({ publicDir, dataDir, packsDir }, { log: /** @type {any} */ (log) });
|
||||
const mounts = [
|
||||
{ prefix: '/data/', name: 'data', dir: path.resolve(dataDir) },
|
||||
{ prefix: '/shared/', name: 'shared', dir: path.resolve(sharedDir) },
|
||||
@@ -74,6 +82,16 @@ export function createStaticHandler({ publicDir, dataDir, sharedDir, simDir = pa
|
||||
res.end(req.method === 'HEAD' ? undefined : shimBody);
|
||||
return;
|
||||
}
|
||||
// Content packs (server/packs.js): the live index, and only the files a pack's manifest names
|
||||
if (decoded.startsWith(PACKS_URL)) {
|
||||
const parts = decoded.slice(PACKS_URL.length).split('/');
|
||||
if (parts.length === 1 && parts[0] === PACK_INDEX_FILE) { sendJson(req, res, 200, registry.index()); return; }
|
||||
const abs = parts.length >= 2 && !parts.some((s) => s === '' || s === '.' || s === '..') ? registry.servable(parts[0], parts.slice(1).join('/')) : null;
|
||||
const st = abs ? await fsp.stat(abs).catch(() => null) : null;
|
||||
if (!st || !st.isFile()) { sendError(req, res, 404, '页面不存在 · Not found', decoded.length <= 200 ? decoded : ''); return; }
|
||||
await serveFile(req, res, abs, st, 'packs', parts, query, gzipCache, log);
|
||||
return;
|
||||
}
|
||||
// Extension-less audio (download-manager avoidance): /media/bgm/act1 → /assets/audio/bgm/act1.mp3
|
||||
if (decoded.startsWith(MEDIA_PREFIX)) {
|
||||
await serveMedia(req, res, decoded.slice(MEDIA_PREFIX.length), query, publicDir, gzipCache, log);
|
||||
|
||||
Reference in new issue
Block a user