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
+33
-22
@@ -10,6 +10,11 @@
|
||||
// the 2-character hash (textHash2) of each Chinese string a leaf replaces, in walk order (object keys in their order,
|
||||
// array entries by index, `_h` itself skipped). A record that is a plain string (config `seasonName`) has { _s, _h }. A leaf whose Chinese source no longer hashes the same — data/*.json was
|
||||
// rebuilt after the overlay — is not applied: the player sees the current Chinese text rather than a stale translation.
|
||||
//
|
||||
// Language packs (shared/i18nPacks.js): a language's game texts may come from several overlays — the pack's own, its
|
||||
// base's, a fallback's (English) — applied per text: `applyFileOverlay(json, overlay, onto)` checks each leaf against
|
||||
// the Chinese `json` and writes it over `onto` (what the overlays of lower priority made), so applying them from the
|
||||
// last of the chain to the first leaves, for every text, the best overlay that has it (public/js/data.js).
|
||||
|
||||
export const OVERLAY_VERSION = 1;
|
||||
|
||||
@@ -106,77 +111,83 @@ export function buildRecordOverlay(leaves) {
|
||||
|
||||
/**
|
||||
* Apply a record overlay to a Chinese record: a copy that shares every untouched branch (copy-on-write), with each leaf
|
||||
* whose Chinese source still matches its hash replaced. The input record is never modified.
|
||||
* whose Chinese source still matches its hash replaced. The input record is never modified. `onto` (default: the
|
||||
* record itself) is what the replaced leaves are written over — the record as overlays of lower priority left it — while
|
||||
* the hashes are always checked against the Chinese `base`.
|
||||
* @param {any} base
|
||||
* @param {any} ov
|
||||
* @param {any} [onto]
|
||||
* @returns {{ value: any, applied: number, stale: number }}
|
||||
*/
|
||||
export function applyRecordOverlay(base, ov) {
|
||||
export function applyRecordOverlay(base, ov, onto = base) {
|
||||
if (isObj(ov) && typeof ov._s === 'string') {
|
||||
const ok = typeof base === 'string' && textHash2(base) === ov._h;
|
||||
return { value: ok ? ov._s : base, applied: ok ? 1 : 0, stale: ok ? 0 : 1 };
|
||||
return { value: ok ? ov._s : onto, applied: ok ? 1 : 0, stale: ok ? 0 : 1 };
|
||||
}
|
||||
if (!isObj(ov) || base == null || typeof base !== 'object') return { value: base, applied: 0, stale: 0 };
|
||||
if (!isObj(ov) || base == null || typeof base !== 'object') return { value: onto, applied: 0, stale: 0 };
|
||||
const hashes = typeof ov._h === 'string' ? ov._h : '';
|
||||
let i = 0;
|
||||
let applied = 0;
|
||||
let stale = 0;
|
||||
const rec = (b, o, depth) => {
|
||||
// b: the Chinese at this path (hash check); t: the target at this path (same shape: overlays replace strings only)
|
||||
const rec = (b, t, o, depth) => {
|
||||
if (typeof o === 'string') {
|
||||
const want = hashes.slice(i * 2, i * 2 + 2);
|
||||
i++;
|
||||
if (typeof b === 'string' && want && textHash2(b) === want) { applied++; return o; }
|
||||
stale++;
|
||||
return b;
|
||||
return t;
|
||||
}
|
||||
if (Array.isArray(o)) {
|
||||
if (!Array.isArray(b)) { walkOverlay(o, () => { i++; stale++; }); return b; }
|
||||
let out = b;
|
||||
if (!Array.isArray(b) || !Array.isArray(t)) { walkOverlay(o, () => { i++; stale++; }); return t; }
|
||||
let out = t;
|
||||
for (let k = 0; k < o.length; k++) {
|
||||
if (o[k] == null) continue;
|
||||
const nv = rec(b[k], o[k], depth + 1);
|
||||
if (nv !== b[k]) { if (out === b) out = b.slice(); out[k] = nv; }
|
||||
const nv = rec(b[k], t[k], o[k], depth + 1);
|
||||
if (nv !== t[k]) { if (out === t) out = t.slice(); out[k] = nv; }
|
||||
}
|
||||
return out;
|
||||
}
|
||||
if (isObj(o)) {
|
||||
if (b == null || typeof b !== 'object') { walkOverlay(o, () => { i++; stale++; }); return b; }
|
||||
if (b == null || typeof b !== 'object' || t == null || typeof t !== 'object') { walkOverlay(o, () => { i++; stale++; }); return t; }
|
||||
// an array record's overlay is keyed by index (see buildRecordOverlay)
|
||||
const arr = Array.isArray(b);
|
||||
let out = b;
|
||||
let out = t;
|
||||
for (const k of Object.keys(o)) {
|
||||
if (depth === 0 && k === '_h') continue;
|
||||
const at = arr ? (/^\d+$/.test(k) ? Number(k) : -1) : k;
|
||||
const cur = at === -1 ? undefined : b[at];
|
||||
const nv = rec(cur, o[k], depth + 1);
|
||||
if (at !== -1 && nv !== cur) { if (out === b) out = arr ? b.slice() : { ...b }; out[at] = nv; }
|
||||
const tcur = at === -1 ? undefined : t[at];
|
||||
const nv = rec(cur, tcur, o[k], depth + 1);
|
||||
if (at !== -1 && nv !== tcur) { if (out === t) out = Array.isArray(t) ? t.slice() : { ...t }; out[at] = nv; }
|
||||
}
|
||||
return out;
|
||||
}
|
||||
return b;
|
||||
return t;
|
||||
};
|
||||
const value = rec(base, ov, 0);
|
||||
const value = rec(base, onto, ov, 0);
|
||||
return { value, applied, stale };
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply a file's overlay to a whole data file (the `{ id: record }` maps of data/*.json; other shapes are returned
|
||||
* unchanged). Untouched records are shared with the input.
|
||||
* unchanged). Untouched records are shared with the input. `onto`: see applyRecordOverlay (a whole file).
|
||||
* @param {any} json the Chinese file
|
||||
* @param {Record<string, any> | null | undefined} fileOverlay `files[<name>]` of the overlay
|
||||
* @param {any} [onto] the file as overlays of lower priority left it (default: the Chinese file)
|
||||
* @returns {{ value: any, applied: number, stale: number }}
|
||||
*/
|
||||
export function applyFileOverlay(json, fileOverlay) {
|
||||
if (!isObj(json) || !isObj(fileOverlay)) return { value: json, applied: 0, stale: 0 };
|
||||
let out = json;
|
||||
export function applyFileOverlay(json, fileOverlay, onto = json) {
|
||||
if (!isObj(json) || !isObj(fileOverlay) || !isObj(onto)) return { value: onto, applied: 0, stale: 0 };
|
||||
let out = onto;
|
||||
let applied = 0;
|
||||
let stale = 0;
|
||||
for (const [id, ov] of Object.entries(fileOverlay)) {
|
||||
if (!Object.prototype.hasOwnProperty.call(json, id)) continue;
|
||||
const r = applyRecordOverlay(json[id], ov);
|
||||
const r = applyRecordOverlay(json[id], ov, onto[id]);
|
||||
applied += r.applied;
|
||||
stale += r.stale;
|
||||
if (r.value !== json[id]) { if (out === json) out = { ...json }; out[id] = r.value; }
|
||||
if (r.value !== onto[id]) { if (out === onto) out = { ...onto }; out[id] = r.value; }
|
||||
}
|
||||
return { value: out, applied, stale };
|
||||
}
|
||||
Reference in new issue
Block a user