@moku-labs/game 0.7.1 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -8
- package/dist/assets.d.mts +97 -33
- package/dist/assets.mjs +350 -110
- package/dist/{commands-mzE9PX0H.d.mts → commands-DuwCxK5t.d.mts} +5 -4
- package/dist/control.d.mts +4 -3
- package/dist/control.mjs +2 -2
- package/dist/{define-BbgXv4cy.d.mts → define-B6pHpdBi.d.mts} +4 -3
- package/dist/headless-BsNdJs9W.mjs +186 -0
- package/dist/headless-SCQXJxQf.d.mts +181 -0
- package/dist/hot.d.mts +4 -2
- package/dist/hot.mjs +8 -6
- package/dist/index-BO9kZzY3.d.mts +1611 -0
- package/dist/index.d.mts +6 -1596
- package/dist/index.mjs +4 -34334
- package/dist/{input-CK4nxBO1.mjs → input-z_35Jwfn.mjs} +2 -2
- package/dist/inspect.d.mts +7 -6
- package/dist/inspect.mjs +2 -2
- package/dist/jsx-dev-runtime.d.mts +1 -1
- package/dist/jsx-runtime.d.mts +1 -1
- package/dist/lint.d.mts +55 -4
- package/dist/lint.mjs +993 -40
- package/dist/project.d.mts +57 -41
- package/dist/project.mjs +994 -505
- package/dist/{run-Bcyg32M5.mjs → run-CofJEdYx.mjs} +4 -187
- package/dist/{runtime-HTlmRb81.d.mts → runtime-CdNJ0UBm.d.mts} +1 -1
- package/dist/{sources-DVWJ8nfr.mjs → sources-DNnzyCYf.mjs} +2 -2
- package/dist/src-536arXjV.mjs +34644 -0
- package/dist/testing.d.mts +85 -283
- package/dist/testing.mjs +158 -2109
- package/dist/{types-BMEn0tSC.d.mts → types-B0v5VVa3.d.mts} +10 -9
- package/dist/{types-ClF8say0.d.mts → types-CQueODzo.d.mts} +2 -2
- package/dist/{types-CYYKjvLn.d.mts → types-DUMoYPYe.d.mts} +5 -182
- package/dist/{types-CXyipKhE.d.mts → types-VZVWwqCg.d.mts} +42 -4
- package/dist/{types-BaUUcIaN.d.mts → types-xXfyk56Q.d.mts} +1 -1
- package/dist/{visual-DUvvPPCt.mjs → visual-dMW8iqAE.mjs} +8 -4
- package/dist/visual.d.mts +330 -0
- package/dist/visual.mjs +2142 -0
- package/llms.txt +59 -21
- package/package.json +7 -8
- package/dist/memory-CvgdnsQO.mjs +0 -259
package/dist/assets.mjs
CHANGED
|
@@ -1894,9 +1894,10 @@ function keyOf(feature, relative, file) {
|
|
|
1894
1894
|
//#endregion
|
|
1895
1895
|
//#region src/plugins/assets/scan/scan.ts
|
|
1896
1896
|
/**
|
|
1897
|
-
* @file assets plugin, build time — the walk. `features/*` with an `assets/` folder
|
|
1898
|
-
*
|
|
1899
|
-
*
|
|
1897
|
+
* @file assets plugin, build time — the walk. `features/*` with an `assets/` folder, and every
|
|
1898
|
+
* layer the options map (a folder under the root read like one more feature), becomes the manifest:
|
|
1899
|
+
* one bundle per feature or layer, split by the optional `assets.ts` it exports. Node and Bun only.
|
|
1900
|
+
* Nothing under `src/` outside `scan/` imports it, so no game bundles it.
|
|
1900
1901
|
*/
|
|
1901
1902
|
const TIERS = [
|
|
1902
1903
|
"boot",
|
|
@@ -1982,6 +1983,75 @@ async function listFeatures(dir) {
|
|
|
1982
1983
|
return (await readdir(dir, { withFileTypes: true }).catch(() => [])).filter((entry) => entry.isDirectory()).map((entry) => entry.name).toSorted(byName$1);
|
|
1983
1984
|
}
|
|
1984
1985
|
/**
|
|
1986
|
+
* Tells whether a path is a folder.
|
|
1987
|
+
*
|
|
1988
|
+
* @param dir - The path to look at.
|
|
1989
|
+
* @returns True for a folder; false for a file or nothing.
|
|
1990
|
+
*/
|
|
1991
|
+
async function isFolder(dir) {
|
|
1992
|
+
return await stat(dir).then((entry) => entry.isDirectory(), () => false);
|
|
1993
|
+
}
|
|
1994
|
+
/**
|
|
1995
|
+
* Says why one layer cannot be read, by its own folder and name alone.
|
|
1996
|
+
*
|
|
1997
|
+
* @param folder - The layer folder under the root.
|
|
1998
|
+
* @param name - The name its keys and bundles take.
|
|
1999
|
+
* @param features - Name of the features folder.
|
|
2000
|
+
* @param featureNames - The feature folders found under it.
|
|
2001
|
+
* @returns The problem, or `undefined` when the layer is fine on its own.
|
|
2002
|
+
* @example
|
|
2003
|
+
* ```ts
|
|
2004
|
+
* layerRefusal("shared", "u.i", "features", []);
|
|
2005
|
+
* // 'the layer "shared" is mapped to "u.i", which is empty or has a "." that would fake a folder.'
|
|
2006
|
+
* ```
|
|
2007
|
+
*/
|
|
2008
|
+
function layerRefusal(folder, name, features, featureNames) {
|
|
2009
|
+
if (folder === "" || /[/\\.]/.test(folder)) return `the layer "${folder}" is not a folder name: ` + String.raw`use a name without "/", "\" or ".".`;
|
|
2010
|
+
if (folder === features) return `the layer "${folder}" is the features folder "${features}"; the features are scanned already.`;
|
|
2011
|
+
if (name === "" || name.includes(".")) return `the layer "${folder}" is mapped to "${name}", which is empty or has a "." that would fake a folder.`;
|
|
2012
|
+
if (featureNames.includes(name)) return `the layer "${folder}" is mapped to "${name}", which is also a feature folder: their keys and bundles would merge.`;
|
|
2013
|
+
}
|
|
2014
|
+
/**
|
|
2015
|
+
* Says that a layer is mapped to a name an earlier layer took.
|
|
2016
|
+
*
|
|
2017
|
+
* @param first - The folder of the earlier layer with that name, if one took it.
|
|
2018
|
+
* @param folder - The layer folder.
|
|
2019
|
+
* @param name - The name it is mapped to.
|
|
2020
|
+
* @returns The refusal, or `undefined` when no earlier layer took the name.
|
|
2021
|
+
* @example
|
|
2022
|
+
* ```ts
|
|
2023
|
+
* nameTakenRefusal("common", "shared", "ui"); // 'the layers "common" and "shared" are both mapped to "ui".'
|
|
2024
|
+
* ```
|
|
2025
|
+
*/
|
|
2026
|
+
function nameTakenRefusal(first, folder, name) {
|
|
2027
|
+
if (first === void 0) return void 0;
|
|
2028
|
+
return `the layers "${first}" and "${folder}" are both mapped to "${name}".`;
|
|
2029
|
+
}
|
|
2030
|
+
/**
|
|
2031
|
+
* Checks every layer of a map, sorted by folder: one refusal at most per layer, and a name that an
|
|
2032
|
+
* earlier layer took is a refusal of the later one.
|
|
2033
|
+
*
|
|
2034
|
+
* @param layers - Folder to name.
|
|
2035
|
+
* @param features - Name of the features folder.
|
|
2036
|
+
* @param featureNames - The feature folders found under it.
|
|
2037
|
+
* @returns Every layer with its refusal, in the order of the sorted folders.
|
|
2038
|
+
*/
|
|
2039
|
+
function checkLayers(layers, features, featureNames) {
|
|
2040
|
+
const taken = /* @__PURE__ */ new Map();
|
|
2041
|
+
const checks = [];
|
|
2042
|
+
const sorted = Object.entries(layers).toSorted((left, right) => byName$1(left[0], right[0]));
|
|
2043
|
+
for (const [folder, name] of sorted) {
|
|
2044
|
+
const refusal = layerRefusal(folder, name, features, featureNames) ?? nameTakenRefusal(taken.get(name), folder, name);
|
|
2045
|
+
if (refusal === void 0) taken.set(name, folder);
|
|
2046
|
+
checks.push({
|
|
2047
|
+
folder,
|
|
2048
|
+
name,
|
|
2049
|
+
refusal
|
|
2050
|
+
});
|
|
2051
|
+
}
|
|
2052
|
+
return checks;
|
|
2053
|
+
}
|
|
2054
|
+
/**
|
|
1985
2055
|
* Walks one `assets/` folder.
|
|
1986
2056
|
*
|
|
1987
2057
|
* @param dir - The folder to read.
|
|
@@ -2064,20 +2134,21 @@ function readSpec(scan, feature, name, raw, specs) {
|
|
|
2064
2134
|
});
|
|
2065
2135
|
}
|
|
2066
2136
|
/**
|
|
2067
|
-
* Reads the bundles a feature declares. Every export that is a `defineBundles` result
|
|
2137
|
+
* Reads the bundles a feature or a layer declares. Every export that is a `defineBundles` result
|
|
2138
|
+
* counts.
|
|
2068
2139
|
*
|
|
2069
2140
|
* @param scan - State of the scan.
|
|
2070
|
-
* @param
|
|
2071
|
-
* @returns The declared bundles, empty when the
|
|
2141
|
+
* @param owner - The feature or layer: its folder and the name its bundles take.
|
|
2142
|
+
* @returns The declared bundles, empty when the owner brought no description.
|
|
2072
2143
|
*/
|
|
2073
|
-
async function readBundleSpecs(scan,
|
|
2144
|
+
async function readBundleSpecs(scan, owner) {
|
|
2074
2145
|
const specs = /* @__PURE__ */ new Map();
|
|
2075
|
-
const file = path.join(
|
|
2146
|
+
const file = path.join(owner.dir, "assets.ts");
|
|
2076
2147
|
try {
|
|
2077
2148
|
const loaded = await importDescription(file);
|
|
2078
2149
|
for (const value of Object.values(loaded ?? {})) {
|
|
2079
2150
|
if (!isRecord(value) || value.kind !== "bundles" || !isRecord(value.map)) continue;
|
|
2080
|
-
for (const [name, raw] of Object.entries(value.map)) readSpec(scan,
|
|
2151
|
+
for (const [name, raw] of Object.entries(value.map)) readSpec(scan, owner.name, name, raw, specs);
|
|
2081
2152
|
}
|
|
2082
2153
|
} catch (error) {
|
|
2083
2154
|
const relative = toPosix(path.relative(scan.root, file));
|
|
@@ -2351,14 +2422,16 @@ async function addFile(scan, pass, relative) {
|
|
|
2351
2422
|
}
|
|
2352
2423
|
}
|
|
2353
2424
|
/**
|
|
2354
|
-
* Reads one feature: its description first, then its fonts, then every file of its
|
|
2425
|
+
* Reads one feature or layer: its description first, then its fonts, then every file of its
|
|
2426
|
+
* `assets/`.
|
|
2355
2427
|
*
|
|
2356
2428
|
* @param scan - State of the scan.
|
|
2357
|
-
* @param
|
|
2429
|
+
* @param owner - The feature or layer: its folder and the name its keys and bundles take.
|
|
2358
2430
|
*/
|
|
2359
|
-
async function
|
|
2360
|
-
const
|
|
2361
|
-
const
|
|
2431
|
+
async function scanOwner(scan, owner) {
|
|
2432
|
+
const feature = owner.name;
|
|
2433
|
+
const specs = await readBundleSpecs(scan, owner);
|
|
2434
|
+
const assetsFolder = path.join(owner.dir, "assets");
|
|
2362
2435
|
for (const [name, spec] of specs) draftOf(scan, name, feature, spec.tier);
|
|
2363
2436
|
const files = await walkAssets(assetsFolder, "");
|
|
2364
2437
|
const fonts = await readFonts(scan, assetsFolder, files);
|
|
@@ -2457,26 +2530,57 @@ async function applyOutputs(outputs, write) {
|
|
|
2457
2530
|
return changed;
|
|
2458
2531
|
}
|
|
2459
2532
|
/**
|
|
2460
|
-
*
|
|
2461
|
-
*
|
|
2533
|
+
* Reads the layers that passed their checks, sorted by folder. A layer without its folder is a
|
|
2534
|
+
* note, not a problem: a typo in `--layer` would otherwise drop its keys without a word.
|
|
2535
|
+
*
|
|
2536
|
+
* @param scan - State of the scan.
|
|
2537
|
+
* @param checks - Every layer with its refusal; a refused layer is not read.
|
|
2538
|
+
*/
|
|
2539
|
+
async function scanLayers(scan, checks) {
|
|
2540
|
+
for (const { folder, name, refusal } of checks) {
|
|
2541
|
+
if (refusal !== void 0) continue;
|
|
2542
|
+
const dir = path.join(scan.root, folder);
|
|
2543
|
+
if (!await isFolder(dir)) {
|
|
2544
|
+
scan.notes.push(`the layer "${folder}" has no folder "${folder}/" under the root; nothing was read for it.`);
|
|
2545
|
+
continue;
|
|
2546
|
+
}
|
|
2547
|
+
await scanOwner(scan, {
|
|
2548
|
+
name,
|
|
2549
|
+
dir
|
|
2550
|
+
});
|
|
2551
|
+
}
|
|
2552
|
+
}
|
|
2553
|
+
/**
|
|
2554
|
+
* Walks the features and the layers of a game and writes its two generated files: one bundle per
|
|
2555
|
+
* feature or layer, split by its optional `assets.ts`, every file keyed by the key rule. A layer is
|
|
2556
|
+
* read like one more feature under its mapped name, after the features. A check run
|
|
2462
2557
|
* (`write: false`) writes nothing and only reports whether something would change.
|
|
2463
2558
|
*
|
|
2464
|
-
* @param options - The game source root, the two output paths and whether to write.
|
|
2559
|
+
* @param options - The game source root, the layers, the two output paths and whether to write.
|
|
2465
2560
|
* @returns The manifest, the text of the key module and whether an output differed.
|
|
2466
|
-
* @throws {Error} One error that lists every problem the scan found.
|
|
2561
|
+
* @throws {Error} One error that lists every problem the scan found, the layer problems first.
|
|
2467
2562
|
* @example
|
|
2468
2563
|
* ```ts
|
|
2469
2564
|
* const { manifest, changed } = await scanAssets({
|
|
2470
2565
|
* root: "src",
|
|
2471
2566
|
* manifest: "public/assets/manifest.json",
|
|
2472
|
-
* keys: "src/generated/assets.ts"
|
|
2567
|
+
* keys: "src/generated/assets.ts",
|
|
2568
|
+
* layers: { shared: "ui" } // shared/assets/button/primary.png keeps the key "ui.button.primary"
|
|
2473
2569
|
* });
|
|
2474
2570
|
* // manifest.bundles.ui.files[0].key: "ui.button.primary", changed: true on the first run
|
|
2475
2571
|
* ```
|
|
2476
2572
|
*/
|
|
2477
2573
|
async function scanAssets(options) {
|
|
2478
|
-
const
|
|
2479
|
-
|
|
2574
|
+
const features = options.features ?? "features";
|
|
2575
|
+
const scan = createState(options.root, features);
|
|
2576
|
+
const featureNames = await listFeatures(scan.featuresFolder);
|
|
2577
|
+
const checks = checkLayers(options.layers ?? {}, features, featureNames);
|
|
2578
|
+
for (const check of checks) if (check.refusal !== void 0) scan.problems.push(check.refusal);
|
|
2579
|
+
for (const feature of featureNames) await scanOwner(scan, {
|
|
2580
|
+
name: feature,
|
|
2581
|
+
dir: path.join(scan.featuresFolder, feature)
|
|
2582
|
+
});
|
|
2583
|
+
await scanLayers(scan, checks);
|
|
2480
2584
|
if (scan.problems.length > 0) throw collected(scan.problems);
|
|
2481
2585
|
const manifest = toManifest(scan);
|
|
2482
2586
|
const manifestSource = emitManifest(manifest);
|
|
@@ -2510,6 +2614,8 @@ const KEYS_FILE = path.join("generated", "assets.ts");
|
|
|
2510
2614
|
const DEFAULT_SOURCE$1 = "en";
|
|
2511
2615
|
/** The extension of an exchange file: the export writes `<dir>/<locale>.json`. */
|
|
2512
2616
|
const EXCHANGE_EXTENSION = ".json";
|
|
2617
|
+
/** The flag that maps a folder under the root to the name its keys take. It repeats. */
|
|
2618
|
+
const LAYER_FLAG = "--layer";
|
|
2513
2619
|
/**
|
|
2514
2620
|
* Wraps a command line problem in the message shape of the framework.
|
|
2515
2621
|
*
|
|
@@ -2547,15 +2653,35 @@ function isSwitch(flag) {
|
|
|
2547
2653
|
return flag === "--check" || flag === "--no-cache" || flag === "--pseudo";
|
|
2548
2654
|
}
|
|
2549
2655
|
/**
|
|
2656
|
+
* Reads the value after a flag that takes one: the next argument, unless it is missing or is
|
|
2657
|
+
* another flag.
|
|
2658
|
+
*
|
|
2659
|
+
* @param argv - The arguments after the script name.
|
|
2660
|
+
* @param at - Where the flag is.
|
|
2661
|
+
* @param what - What the value is, as the refusal names it: `a path`, `a locale`.
|
|
2662
|
+
* @returns The value.
|
|
2663
|
+
* @throws {Error} When the value is missing or is another flag.
|
|
2664
|
+
* @example
|
|
2665
|
+
* ```ts
|
|
2666
|
+
* valueAt(["--root", "src"], 0, "a path"); // "src"
|
|
2667
|
+
* ```
|
|
2668
|
+
*/
|
|
2669
|
+
function valueAt(argv, at, what) {
|
|
2670
|
+
const value = argv[at + 1];
|
|
2671
|
+
if (value === void 0 || value.startsWith("--")) throw problem$1(`"${String(argv[at])}" needs ${what}.`);
|
|
2672
|
+
return value;
|
|
2673
|
+
}
|
|
2674
|
+
/**
|
|
2550
2675
|
* Reads the flags as they were given.
|
|
2551
2676
|
*
|
|
2552
2677
|
* @param argv - The arguments after the script name.
|
|
2553
|
-
* @returns The value of each flag that takes one, and the
|
|
2678
|
+
* @returns The value of each flag that takes one, the switches and the `--layer` values.
|
|
2554
2679
|
* @throws {Error} When an option is unknown or misses its value.
|
|
2555
2680
|
*/
|
|
2556
2681
|
function readFlags(argv) {
|
|
2557
2682
|
const values = {};
|
|
2558
2683
|
const switches = /* @__PURE__ */ new Set();
|
|
2684
|
+
const layers = [];
|
|
2559
2685
|
let index = 0;
|
|
2560
2686
|
while (index < argv.length) {
|
|
2561
2687
|
const flag = argv[index];
|
|
@@ -2564,18 +2690,62 @@ function readFlags(argv) {
|
|
|
2564
2690
|
index += 1;
|
|
2565
2691
|
continue;
|
|
2566
2692
|
}
|
|
2693
|
+
if (flag === LAYER_FLAG) {
|
|
2694
|
+
layers.push(valueAt(argv, index, "\"<folder>[=<name>]\""));
|
|
2695
|
+
index += 2;
|
|
2696
|
+
continue;
|
|
2697
|
+
}
|
|
2567
2698
|
if (!isValueFlag(flag)) throw problem$1(`unknown option "${String(flag)}".`);
|
|
2568
|
-
|
|
2569
|
-
if (value === void 0 || value.startsWith("--")) throw problem$1(`"${flag}" needs ${flag === "--source" ? "a locale" : "a path"}.`);
|
|
2570
|
-
values[flag] = value;
|
|
2699
|
+
values[flag] = valueAt(argv, index, flag === "--source" ? "a locale" : "a path");
|
|
2571
2700
|
index += 2;
|
|
2572
2701
|
}
|
|
2573
2702
|
return {
|
|
2574
2703
|
values,
|
|
2575
|
-
switches
|
|
2704
|
+
switches,
|
|
2705
|
+
layers
|
|
2576
2706
|
};
|
|
2577
2707
|
}
|
|
2578
2708
|
/**
|
|
2709
|
+
* Reads the `--layer` values into the layer map. `shared=ui` maps the folder to the name,
|
|
2710
|
+
* `shared` alone maps it to itself. Only the syntax is checked here; the scan refuses a folder or
|
|
2711
|
+
* a name that cannot be a layer.
|
|
2712
|
+
*
|
|
2713
|
+
* @param raw - The values as given, in order.
|
|
2714
|
+
* @returns Folder to name.
|
|
2715
|
+
* @throws {Error} When a value has an empty side or a folder repeats.
|
|
2716
|
+
* @example
|
|
2717
|
+
* ```ts
|
|
2718
|
+
* parseLayers(["shared=ui", "common"]); // { shared: "ui", common: "common" }
|
|
2719
|
+
* ```
|
|
2720
|
+
*/
|
|
2721
|
+
function parseLayers(raw) {
|
|
2722
|
+
const layers = /* @__PURE__ */ new Map();
|
|
2723
|
+
for (const value of raw) {
|
|
2724
|
+
const cut = value.indexOf("=");
|
|
2725
|
+
const folder = cut === -1 ? value : value.slice(0, cut);
|
|
2726
|
+
const name = cut === -1 ? value : value.slice(cut + 1);
|
|
2727
|
+
if (folder === "" || name === "" || name.includes("=")) throw problem$1(`"--layer ${value}" needs a folder and a name: "<folder>[=<name>]".`);
|
|
2728
|
+
if (layers.has(folder)) throw problem$1(`"--layer" names the folder "${folder}" twice.`);
|
|
2729
|
+
layers.set(folder, name);
|
|
2730
|
+
}
|
|
2731
|
+
return Object.fromEntries(layers);
|
|
2732
|
+
}
|
|
2733
|
+
/**
|
|
2734
|
+
* The layer member of the options a tool is handed: none for a run without `--layer`, so such a
|
|
2735
|
+
* run calls every tool exactly as before layers existed.
|
|
2736
|
+
*
|
|
2737
|
+
* @param layers - The layers of the run.
|
|
2738
|
+
* @returns `{ layers }`, or an empty object when there is no layer.
|
|
2739
|
+
* @example
|
|
2740
|
+
* ```ts
|
|
2741
|
+
* withLayers({}); // {}
|
|
2742
|
+
* withLayers({ shared: "ui" }); // { layers: { shared: "ui" } }
|
|
2743
|
+
* ```
|
|
2744
|
+
*/
|
|
2745
|
+
function withLayers(layers) {
|
|
2746
|
+
return Object.keys(layers).length === 0 ? {} : { layers };
|
|
2747
|
+
}
|
|
2748
|
+
/**
|
|
2579
2749
|
* Refuses the flags that do not go together, before anything is read or written. An exchange
|
|
2580
2750
|
* with the translators runs alone; only `--pseudo` may join an import, because the compile after
|
|
2581
2751
|
* it writes the pseudo-locale too.
|
|
@@ -2610,8 +2780,9 @@ function resolveGiven(given) {
|
|
|
2610
2780
|
* `manifest.json` inside the root, or inside the pack folder with `--pack`; `--source` to `"en"`.
|
|
2611
2781
|
*
|
|
2612
2782
|
* @param argv - The arguments after the script name.
|
|
2613
|
-
* @returns The resolved paths, the folders of the three modes and the
|
|
2614
|
-
* @throws {Error} When an option is unknown, misses its value
|
|
2783
|
+
* @returns The resolved paths, the folders of the three modes, the switches and the layers.
|
|
2784
|
+
* @throws {Error} When an option is unknown, misses its value, does not go with another, or a
|
|
2785
|
+
* `--layer` value is not `<folder>[=<name>]`.
|
|
2615
2786
|
*/
|
|
2616
2787
|
function parseArgv(argv) {
|
|
2617
2788
|
const flags = readFlags(argv);
|
|
@@ -2629,7 +2800,8 @@ function parseArgv(argv) {
|
|
|
2629
2800
|
pseudo: switches.has("--pseudo"),
|
|
2630
2801
|
source: values["--source"] ?? DEFAULT_SOURCE$1,
|
|
2631
2802
|
exportDir: resolveGiven(values["--export"]),
|
|
2632
|
-
importDir: resolveGiven(values["--import"])
|
|
2803
|
+
importDir: resolveGiven(values["--import"]),
|
|
2804
|
+
layers: parseLayers(flags.layers)
|
|
2633
2805
|
};
|
|
2634
2806
|
}
|
|
2635
2807
|
/**
|
|
@@ -2747,9 +2919,9 @@ function packSummary(file, result, cache) {
|
|
|
2747
2919
|
return `wrote "${file}": ${plural(result.pages, "page")}, ${plural(result.loose, "loose file")}, ${plural(fonts, "font")}, ${plural(sounds, "sound")}, ${Math.round(result.bytes / 1024)} KB. cache: ${hits}.`;
|
|
2748
2920
|
}
|
|
2749
2921
|
/**
|
|
2750
|
-
* The dev run of `bun run assets:keys`: walk the features, write the manifest and
|
|
2751
|
-
*
|
|
2752
|
-
* nothing is written and a difference fails the run.
|
|
2922
|
+
* The dev run of `bun run assets:keys`: walk the features and the layers, write the manifest and
|
|
2923
|
+
* the key module, then compile the strings next to the key module (the pseudo-locale too with
|
|
2924
|
+
* `--pseudo`). With `--check` nothing is written and a difference fails the run.
|
|
2753
2925
|
*
|
|
2754
2926
|
* @param options - The parsed flags.
|
|
2755
2927
|
* @param compile - The strings compiler.
|
|
@@ -2762,11 +2934,13 @@ async function runKeys(options, compile, ui) {
|
|
|
2762
2934
|
root: options.root,
|
|
2763
2935
|
manifest: options.manifest,
|
|
2764
2936
|
keys: options.keys,
|
|
2765
|
-
write: !options.check
|
|
2937
|
+
write: !options.check,
|
|
2938
|
+
...withLayers(options.layers)
|
|
2766
2939
|
});
|
|
2767
2940
|
const strings = await compile(options.root, path.dirname(options.keys), {
|
|
2768
2941
|
check: options.check,
|
|
2769
|
-
pseudo: options.pseudo
|
|
2942
|
+
pseudo: options.pseudo,
|
|
2943
|
+
...withLayers(options.layers)
|
|
2770
2944
|
});
|
|
2771
2945
|
for (const note of [...notes, ...strings.notes]) ui.warn(note);
|
|
2772
2946
|
if ((changed || strings.changed) && options.check) {
|
|
@@ -2795,7 +2969,8 @@ async function runPack(options, pack, compile, ui) {
|
|
|
2795
2969
|
root: options.root,
|
|
2796
2970
|
manifest: options.manifest,
|
|
2797
2971
|
keys: options.keys,
|
|
2798
|
-
write: false
|
|
2972
|
+
write: false,
|
|
2973
|
+
...withLayers(options.layers)
|
|
2799
2974
|
});
|
|
2800
2975
|
await applyOutputs([{
|
|
2801
2976
|
file: options.keys,
|
|
@@ -2803,7 +2978,8 @@ async function runPack(options, pack, compile, ui) {
|
|
|
2803
2978
|
}], true);
|
|
2804
2979
|
const strings = await compile(options.root, path.dirname(options.keys), {
|
|
2805
2980
|
check: false,
|
|
2806
|
-
pseudo: false
|
|
2981
|
+
pseudo: false,
|
|
2982
|
+
...withLayers(options.layers)
|
|
2807
2983
|
});
|
|
2808
2984
|
for (const note of [...scan.notes, ...strings.notes]) ui.warn(note);
|
|
2809
2985
|
const result = await packAssets({
|
|
@@ -2829,7 +3005,10 @@ async function runPack(options, pack, compile, ui) {
|
|
|
2829
3005
|
* @throws {Error} When the export fails.
|
|
2830
3006
|
*/
|
|
2831
3007
|
async function runExport(options, dir, exportStrings, ui) {
|
|
2832
|
-
const report = await exportStrings(options.root, dir, {
|
|
3008
|
+
const report = await exportStrings(options.root, dir, {
|
|
3009
|
+
source: options.source,
|
|
3010
|
+
...withLayers(options.layers)
|
|
3011
|
+
});
|
|
2833
3012
|
for (const locale of report.locales) ui.info(exportLine(dir, locale, report.missing[locale] ?? 0));
|
|
2834
3013
|
return 0;
|
|
2835
3014
|
}
|
|
@@ -2848,7 +3027,8 @@ async function runImport(options, dir, importStrings, ui) {
|
|
|
2848
3027
|
const report = await importStrings(options.root, dir, {
|
|
2849
3028
|
source: options.source,
|
|
2850
3029
|
pseudo: options.pseudo,
|
|
2851
|
-
out: path.dirname(options.keys)
|
|
3030
|
+
out: path.dirname(options.keys),
|
|
3031
|
+
...withLayers(options.layers)
|
|
2852
3032
|
});
|
|
2853
3033
|
ui.info(importLine(dir, report));
|
|
2854
3034
|
return 0;
|
|
@@ -2858,8 +3038,10 @@ async function runImport(options, dir, importStrings, ui) {
|
|
|
2858
3038
|
* compile the feature strings next to the key module, or check that all of it is current. With
|
|
2859
3039
|
* `--pack <dir>` it packs the art for production on the same scan (`--no-cache` for a cold run);
|
|
2860
3040
|
* `--pseudo` adds the pseudo-locale to a dev run. `--export <dir>` and `--import <dir>` exchange
|
|
2861
|
-
* the strings with translators and run alone, from the locale of `--source`.
|
|
2862
|
-
* `
|
|
3041
|
+
* the strings with translators and run alone, from the locale of `--source`.
|
|
3042
|
+
* `--layer <folder>[=<name>]` (repeatable) scans a folder under the root like one more feature,
|
|
3043
|
+
* its keys and bundles under `<name>` (`<folder>` by default); it goes with every mode. Nothing
|
|
3044
|
+
* here calls `process.exit`; the caller does.
|
|
2863
3045
|
*
|
|
2864
3046
|
* @param argv - The arguments after the script name.
|
|
2865
3047
|
* @param strings - The string tools of i18n: compile, export and import.
|
|
@@ -2873,6 +3055,8 @@ async function runImport(options, dir, importStrings, ui) {
|
|
|
2873
3055
|
* // 0, and dist/assets holds the pages, the loose files and the packed manifest.json
|
|
2874
3056
|
* await runCli(["--root", "src", "--export", "translations"], strings);
|
|
2875
3057
|
* // 0, and translations/ holds one <locale>.json per locale for the translators
|
|
3058
|
+
* await runCli(["--root", "src", "--layer", "shared=ui"], strings);
|
|
3059
|
+
* // 0, shared/assets/* keyed ui.*, shared/strings/* compiled
|
|
2876
3060
|
* ```
|
|
2877
3061
|
*/
|
|
2878
3062
|
async function runCli(argv, strings, ui = createBrandConsole()) {
|
|
@@ -6309,13 +6493,16 @@ function pseudoMessage(elements) {
|
|
|
6309
6493
|
//#endregion
|
|
6310
6494
|
//#region src/plugins/i18n/compile/walk.ts
|
|
6311
6495
|
/**
|
|
6312
|
-
* @file i18n plugin, build time — the walk over `features/*/strings/<locale>.json
|
|
6313
|
-
* read, every key claimed by one feature
|
|
6314
|
-
* with the text and the translator note of each
|
|
6315
|
-
* and `importStrings` all start from it. Node and
|
|
6496
|
+
* @file i18n plugin, build time — the walk over `features/*/strings/<locale>.json` and the
|
|
6497
|
+
* `strings/` of every layer: every file read, every key claimed by one feature or layer, every
|
|
6498
|
+
* message compiled once and every problem collected, with the text and the translator note of each
|
|
6499
|
+
* key kept aside. `compileStrings`, `exportStrings` and `importStrings` all start from it. Node and
|
|
6500
|
+
* Bun only.
|
|
6316
6501
|
*/
|
|
6317
6502
|
/** How every problem of the string compiler is prefixed. */
|
|
6318
6503
|
const PREFIX = "[game] i18n: ";
|
|
6504
|
+
/** What a layer folder may not hold: a separator or a ".", which would lead out of the root. */
|
|
6505
|
+
const NOT_ONE_FOLDER = /[/\\.]/;
|
|
6319
6506
|
/**
|
|
6320
6507
|
* Creates the table of compiled messages per locale. Its own function because lint rule L5
|
|
6321
6508
|
* refuses a collection built inside an exported declaration.
|
|
@@ -6459,24 +6646,53 @@ async function listLocales(folder) {
|
|
|
6459
6646
|
}
|
|
6460
6647
|
}
|
|
6461
6648
|
/**
|
|
6462
|
-
* Lists
|
|
6649
|
+
* Lists the string files of one feature or layer, locales sorted. A folder without `strings/`
|
|
6650
|
+
* yields nothing.
|
|
6651
|
+
*
|
|
6652
|
+
* @param root - Game source root.
|
|
6653
|
+
* @param feature - The label: the feature folder name, or the mapped name of a layer.
|
|
6654
|
+
* @param folder - Root-relative POSIX path of the owner folder.
|
|
6655
|
+
* @returns The files of that folder.
|
|
6656
|
+
*/
|
|
6657
|
+
async function listOwnerFiles(root, feature, folder) {
|
|
6658
|
+
const stringsFolder = path.join(root, ...folder.split("/"), "strings");
|
|
6659
|
+
return (await listLocales(stringsFolder)).map((locale) => ({
|
|
6660
|
+
feature,
|
|
6661
|
+
folder,
|
|
6662
|
+
locale,
|
|
6663
|
+
file: path.join(stringsFolder, `${locale}.json`),
|
|
6664
|
+
relative: `${folder}/strings/${locale}.json`
|
|
6665
|
+
}));
|
|
6666
|
+
}
|
|
6667
|
+
/**
|
|
6668
|
+
* The error for a layer folder that is not one folder name under the root: empty, or with a
|
|
6669
|
+
* separator or a "." that would read and write outside it.
|
|
6670
|
+
*
|
|
6671
|
+
* @param folder - The layer folder as given.
|
|
6672
|
+
* @returns The error to throw.
|
|
6673
|
+
*/
|
|
6674
|
+
function notAFolderName(folder) {
|
|
6675
|
+
return /* @__PURE__ */ new Error(`${PREFIX}the layer "${folder}" is not a folder name.\n` + String.raw` Use a name without "/", "\" or ".".`);
|
|
6676
|
+
}
|
|
6677
|
+
/**
|
|
6678
|
+
* Lists every string file of a game: features sorted, then the layers sorted by folder; inside
|
|
6679
|
+
* each, locales sorted. Every layer folder is checked before anything is read.
|
|
6463
6680
|
*
|
|
6464
6681
|
* @param root - Game source root, the folder that holds the features.
|
|
6465
6682
|
* @param features - Name of the folder that holds the features.
|
|
6683
|
+
* @param layers - Folder under the root to the name its keys take. Default `{}`: features only.
|
|
6466
6684
|
* @returns The files, in walk order.
|
|
6685
|
+
* @throws {Error} When a layer folder is empty or holds "/", "\" or ".".
|
|
6467
6686
|
*/
|
|
6468
|
-
async function listStringFiles(root, features) {
|
|
6687
|
+
async function listStringFiles(root, features, layers = {}) {
|
|
6688
|
+
const sortedLayers = Object.entries(layers).toSorted((left, right) => left[0] < right[0] ? -1 : 1);
|
|
6689
|
+
const outside = sortedLayers.find(([folder]) => folder === "" || NOT_ONE_FOLDER.test(folder));
|
|
6690
|
+
if (outside !== void 0) throw notAFolderName(outside[0]);
|
|
6469
6691
|
const files = [];
|
|
6470
6692
|
const featuresFolder = path.join(root, features);
|
|
6471
|
-
|
|
6472
|
-
|
|
6473
|
-
|
|
6474
|
-
feature,
|
|
6475
|
-
locale,
|
|
6476
|
-
file: path.join(stringsFolder, `${locale}.json`),
|
|
6477
|
-
relative: `features/${feature}/strings/${locale}.json`
|
|
6478
|
-
});
|
|
6479
|
-
}
|
|
6693
|
+
const featuresPath = features.split(path.sep).join("/");
|
|
6694
|
+
for (const feature of await listFolders(featuresFolder)) files.push(...await listOwnerFiles(root, feature, `${featuresPath}/${feature}`));
|
|
6695
|
+
for (const [folder, name] of sortedLayers) files.push(...await listOwnerFiles(root, name, folder));
|
|
6480
6696
|
return files;
|
|
6481
6697
|
}
|
|
6482
6698
|
/**
|
|
@@ -6501,12 +6717,13 @@ async function readTable(file, relative, walk) {
|
|
|
6501
6717
|
}
|
|
6502
6718
|
}
|
|
6503
6719
|
/**
|
|
6504
|
-
* Records which feature owns a key, refusing the same key in two
|
|
6720
|
+
* Records which feature or layer owns a key, refusing the same key in two folders. Folders are
|
|
6721
|
+
* compared, not labels: a layer mapped to `ui` and the feature folder `features/ui` are two owners.
|
|
6505
6722
|
*
|
|
6506
6723
|
* @param walk - What the walk collects.
|
|
6507
6724
|
* @param key - The message key.
|
|
6508
6725
|
* @param source - Where this message came from.
|
|
6509
|
-
* @returns True when the key belongs to this
|
|
6726
|
+
* @returns True when the key belongs to this folder.
|
|
6510
6727
|
*/
|
|
6511
6728
|
function claim(walk, key, source) {
|
|
6512
6729
|
const known = walk.owner.get(key);
|
|
@@ -6514,7 +6731,7 @@ function claim(walk, key, source) {
|
|
|
6514
6731
|
walk.owner.set(key, source);
|
|
6515
6732
|
return true;
|
|
6516
6733
|
}
|
|
6517
|
-
if (known.
|
|
6734
|
+
if (known.folder === source.folder) return true;
|
|
6518
6735
|
walk.problems.push(`the key "${key}" is in ${known.relative} and ${source.relative}.`);
|
|
6519
6736
|
return false;
|
|
6520
6737
|
}
|
|
@@ -6608,15 +6825,17 @@ function compileOne(walk, key, value, source) {
|
|
|
6608
6825
|
}
|
|
6609
6826
|
}
|
|
6610
6827
|
/**
|
|
6611
|
-
* Walks every string file of a game
|
|
6612
|
-
* thrown, so the caller reports them all in one error.
|
|
6828
|
+
* Walks every string file of a game, features first and then the layers, and compiles every
|
|
6829
|
+
* message once. Problems are collected, not thrown, so the caller reports them all in one error.
|
|
6613
6830
|
*
|
|
6614
6831
|
* @param root - Game source root, the folder that holds the features.
|
|
6615
6832
|
* @param features - Name of the folder that holds the features.
|
|
6833
|
+
* @param layers - Folder under the root to the name its keys take. Default `{}`: features only.
|
|
6616
6834
|
* @returns The compiled messages per locale, the owner and parameters of each key, the texts and
|
|
6617
6835
|
* notes of each key per locale, and every problem.
|
|
6836
|
+
* @throws {Error} When a layer folder is empty or holds "/", "\" or ".": nothing is read.
|
|
6618
6837
|
*/
|
|
6619
|
-
async function walkStrings(root, features) {
|
|
6838
|
+
async function walkStrings(root, features, layers = {}) {
|
|
6620
6839
|
const walk = {
|
|
6621
6840
|
byLocale: emptyLocales(),
|
|
6622
6841
|
owner: emptyOwners(),
|
|
@@ -6625,11 +6844,12 @@ async function walkStrings(root, features) {
|
|
|
6625
6844
|
texts: emptyTexts(),
|
|
6626
6845
|
notes: emptyTexts()
|
|
6627
6846
|
};
|
|
6628
|
-
for (const file of await listStringFiles(root, features)) {
|
|
6847
|
+
for (const file of await listStringFiles(root, features, layers)) {
|
|
6629
6848
|
const table = await readTable(file.file, file.relative, walk);
|
|
6630
6849
|
if (table === void 0) continue;
|
|
6631
6850
|
const source = {
|
|
6632
6851
|
feature: file.feature,
|
|
6852
|
+
folder: file.folder,
|
|
6633
6853
|
locale: file.locale,
|
|
6634
6854
|
relative: file.relative
|
|
6635
6855
|
};
|
|
@@ -6641,10 +6861,10 @@ async function walkStrings(root, features) {
|
|
|
6641
6861
|
//#region src/plugins/i18n/compile/compile.ts
|
|
6642
6862
|
/**
|
|
6643
6863
|
* @file i18n plugin, build time — `compileStrings`: the walk over `features/*/strings/<locale>.json`
|
|
6644
|
-
* turned into the generated modules, and on request the
|
|
6645
|
-
* Every problem of a run is collected and reported in
|
|
6646
|
-
* one round. Node and Bun only: nothing under `src/`
|
|
6647
|
-
* imports it, so no game bundles the ICU parser.
|
|
6864
|
+
* and the `strings/` of every layer turned into the generated modules, and on request the
|
|
6865
|
+
* pseudo-locale `en-XA` derived from English. Every problem of a run is collected and reported in
|
|
6866
|
+
* one error, so a game fixes its messages in one round. Node and Bun only: nothing under `src/`
|
|
6867
|
+
* outside this folder and `src/assets.ts` imports it, so no game bundles the ICU parser.
|
|
6648
6868
|
*/
|
|
6649
6869
|
/** The locale the pseudo-locale is derived from. */
|
|
6650
6870
|
const PSEUDO_SOURCE = "en";
|
|
@@ -6659,15 +6879,16 @@ function pseudoNeedsEnglish() {
|
|
|
6659
6879
|
return /* @__PURE__ */ new Error(`${PREFIX}"--pseudo" needs an "en" string file in at least one feature.\n Add features/<feature>/strings/en.json or drop "--pseudo".`);
|
|
6660
6880
|
}
|
|
6661
6881
|
/**
|
|
6662
|
-
* Tells whether at least one feature brings a string file for a locale.
|
|
6882
|
+
* Tells whether at least one feature or layer brings a string file for a locale.
|
|
6663
6883
|
*
|
|
6664
6884
|
* @param root - Game source root, the folder that holds the features.
|
|
6665
6885
|
* @param features - Name of the folder that holds the features.
|
|
6886
|
+
* @param layers - Folder under the root to the name its keys take.
|
|
6666
6887
|
* @param locale - The locale to look for.
|
|
6667
|
-
* @returns True when a feature has `strings/<locale>.json`.
|
|
6888
|
+
* @returns True when a feature or a layer has `strings/<locale>.json`.
|
|
6668
6889
|
*/
|
|
6669
|
-
async function bringsLocale(root, features, locale) {
|
|
6670
|
-
return (await listStringFiles(root, features)).some((file) => file.locale === locale);
|
|
6890
|
+
async function bringsLocale(root, features, layers, locale) {
|
|
6891
|
+
return (await listStringFiles(root, features, layers)).some((file) => file.locale === locale);
|
|
6671
6892
|
}
|
|
6672
6893
|
/**
|
|
6673
6894
|
* Derives the pseudo message of every English message. Each one compiled already in the walk, so
|
|
@@ -6712,7 +6933,7 @@ function missingNotes(walk, locales) {
|
|
|
6712
6933
|
const notes = [];
|
|
6713
6934
|
for (const [key, source] of [...walk.owner].toSorted((left, right) => left[0] < right[0] ? -1 : 1)) for (const locale of locales) {
|
|
6714
6935
|
if ((walk.byLocale.get(locale) ?? []).some((entry) => entry.key === key)) continue;
|
|
6715
|
-
notes.push(`the key "${key}" is missing from
|
|
6936
|
+
notes.push(`the key "${key}" is missing from ${source.folder}/strings/${locale}.json.`);
|
|
6716
6937
|
}
|
|
6717
6938
|
return notes;
|
|
6718
6939
|
}
|
|
@@ -6735,30 +6956,37 @@ function typeEntries(walk) {
|
|
|
6735
6956
|
return entries;
|
|
6736
6957
|
}
|
|
6737
6958
|
/**
|
|
6738
|
-
* Walks `features/*/strings/<locale>.json` of a game
|
|
6739
|
-
* generated modules: the types module `strings.ts` and one
|
|
6740
|
-
* `strings.en-XA.ts` with `pseudo`. A check run writes
|
|
6741
|
-
* would change; it checks `strings.en-XA.ts` only with
|
|
6959
|
+
* Walks `features/*/strings/<locale>.json` of a game and the `strings/` of every layer, compiles
|
|
6960
|
+
* every message and writes the generated modules: the types module `strings.ts` and one
|
|
6961
|
+
* `strings.<locale>.ts` per locale, plus `strings.en-XA.ts` with `pseudo`. A check run writes
|
|
6962
|
+
* nothing and only reports whether something would change; it checks `strings.en-XA.ts` only with
|
|
6963
|
+
* `pseudo`.
|
|
6742
6964
|
*
|
|
6743
6965
|
* @param root - Game source root, the folder that holds the features.
|
|
6744
6966
|
* @param out - The generated directory the modules are written into.
|
|
6745
|
-
* @param options - Whether this is a check run, what the features folder is called,
|
|
6746
|
-
*
|
|
6967
|
+
* @param options - Whether this is a check run, what the features folder is called, whether the
|
|
6968
|
+
* pseudo-locale is written, and the layers.
|
|
6747
6969
|
* @returns Whether an output differed, the locales, the keys and the notes.
|
|
6748
|
-
* @throws {Error} One error that lists every problem the compile found,
|
|
6749
|
-
* no feature brings an `en` string file
|
|
6970
|
+
* @throws {Error} One error that lists every problem the compile found, the pseudo error when
|
|
6971
|
+
* no feature or layer brings an `en` string file, or the layer error when a layer folder is not
|
|
6972
|
+
* one folder name: nothing is read then.
|
|
6750
6973
|
* @example
|
|
6751
6974
|
* ```ts
|
|
6752
6975
|
* // A dev build also writes the pseudo-locale, derived from the English messages.
|
|
6753
6976
|
* const report = await compileStrings("src", "src/generated", { pseudo: true });
|
|
6754
6977
|
* // report.locales: ["en", "en-XA", "ru"], report.changed: true on the first run
|
|
6978
|
+
* // A v15 game: features/board/strings/en.json { "board.full": "Full" } and the layer file
|
|
6979
|
+
* // shared/strings/en.json { "ui.ok": "OK" } compile together.
|
|
6980
|
+
* const layered = await compileStrings("src", "src/generated", { layers: { shared: "ui" } });
|
|
6981
|
+
* // layered.keys: ["board.full", "ui.ok"]
|
|
6755
6982
|
* ```
|
|
6756
6983
|
*/
|
|
6757
6984
|
async function compileStrings(root, out, options = {}) {
|
|
6758
6985
|
const features = options.features ?? "features";
|
|
6986
|
+
const layers = options.layers ?? {};
|
|
6759
6987
|
const pseudo = options.pseudo === true;
|
|
6760
|
-
if (pseudo && !await bringsLocale(root, features, PSEUDO_SOURCE)) throw pseudoNeedsEnglish();
|
|
6761
|
-
const walk = await walkStrings(root, features);
|
|
6988
|
+
if (pseudo && !await bringsLocale(root, features, layers, PSEUDO_SOURCE)) throw pseudoNeedsEnglish();
|
|
6989
|
+
const walk = await walkStrings(root, features, layers);
|
|
6762
6990
|
if (walk.problems.length > 0) throw compileFailure(walk.problems);
|
|
6763
6991
|
const brought = [...walk.byLocale.keys()].toSorted();
|
|
6764
6992
|
const keys = [...walk.owner.keys()].toSorted();
|
|
@@ -6789,7 +7017,8 @@ async function compileStrings(root, out, options = {}) {
|
|
|
6789
7017
|
}
|
|
6790
7018
|
/**
|
|
6791
7019
|
* Tells whether the generated modules of a game are current, without writing anything. The
|
|
6792
|
-
* `--check` run of `bun run assets:keys` covers the strings with it.
|
|
7020
|
+
* `--check` run of `bun run assets:keys` covers the strings with it. It walks the features only: a
|
|
7021
|
+
* game with layers checks through `runCli` with `--check --layer …`.
|
|
6793
7022
|
*
|
|
6794
7023
|
* @param root - Game source root, the folder that holds the features.
|
|
6795
7024
|
* @param out - The generated directory the modules live in.
|
|
@@ -6808,8 +7037,8 @@ async function checkStrings(root, out) {
|
|
|
6808
7037
|
/**
|
|
6809
7038
|
* @file i18n plugin, build time — the exchange with translators. `exportStrings` writes one JSON
|
|
6810
7039
|
* file per locale with every key, the source text and the note beside it; `importStrings` checks
|
|
6811
|
-
* the translated files against the game, writes them into the string files of the features
|
|
6812
|
-
* own the keys, and compiles. Node and Bun only, reached through `src/assets.ts`.
|
|
7040
|
+
* the translated files against the game, writes them into the string files of the features and
|
|
7041
|
+
* layers that own the keys, and compiles. Node and Bun only, reached through `src/assets.ts`.
|
|
6813
7042
|
*/
|
|
6814
7043
|
/** The locale translators read from when none is named. */
|
|
6815
7044
|
const DEFAULT_SOURCE = "en";
|
|
@@ -6858,11 +7087,13 @@ function importFailure(problems) {
|
|
|
6858
7087
|
*
|
|
6859
7088
|
* @param root - Game source root, the folder that holds the features.
|
|
6860
7089
|
* @param features - Name of the folder that holds the features.
|
|
7090
|
+
* @param layers - Folder under the root to the name its keys take.
|
|
6861
7091
|
* @returns What the walk collected.
|
|
6862
|
-
* @throws {Error} One error that lists every problem the compile found
|
|
7092
|
+
* @throws {Error} One error that lists every problem the compile found, or the layer error when a
|
|
7093
|
+
* layer folder is not one folder name.
|
|
6863
7094
|
*/
|
|
6864
|
-
async function readGame(root, features) {
|
|
6865
|
-
const walk = await walkStrings(root, features);
|
|
7095
|
+
async function readGame(root, features, layers) {
|
|
7096
|
+
const walk = await walkStrings(root, features, layers);
|
|
6866
7097
|
if (walk.problems.length > 0) throw compileFailure(walk.problems);
|
|
6867
7098
|
return walk;
|
|
6868
7099
|
}
|
|
@@ -6890,16 +7121,17 @@ function exchangeEntry(walk, key, locale, source) {
|
|
|
6890
7121
|
};
|
|
6891
7122
|
}
|
|
6892
7123
|
/**
|
|
6893
|
-
* Writes the files translators work in: one `<dir>/<locale>.json` per locale the features
|
|
6894
|
-
* the source locale included, so a new language starts as a copy of `en.json`. Every
|
|
6895
|
-
* game is in every file, sorted: `{ text, source, note? }`, with `text: ""` where the
|
|
6896
|
-
* message yet. The game tree is not touched, and a second run writes the same bytes.
|
|
7124
|
+
* Writes the files translators work in: one `<dir>/<locale>.json` per locale the features and
|
|
7125
|
+
* layers bring, the source locale included, so a new language starts as a copy of `en.json`. Every
|
|
7126
|
+
* key of the game is in every file, sorted: `{ text, source, note? }`, with `text: ""` where the
|
|
7127
|
+
* locale has no message yet. The game tree is not touched, and a second run writes the same bytes.
|
|
6897
7128
|
*
|
|
6898
7129
|
* @param root - Game source root, the folder that holds the features.
|
|
6899
7130
|
* @param dir - The folder the exchange files are written into; created when missing.
|
|
6900
|
-
* @param options - The source locale
|
|
7131
|
+
* @param options - The source locale, the name of the features folder and the layers.
|
|
6901
7132
|
* @returns The exported locales and how many keys each one lacks.
|
|
6902
|
-
* @throws {Error} When the game's strings do not compile,
|
|
7133
|
+
* @throws {Error} When the game's strings do not compile, a layer folder is not one folder name,
|
|
7134
|
+
* or no feature brings the source locale.
|
|
6903
7135
|
* @example
|
|
6904
7136
|
* ```ts
|
|
6905
7137
|
* // Before a release, the game hands its strings to the translators.
|
|
@@ -6909,7 +7141,7 @@ function exchangeEntry(walk, key, locale, source) {
|
|
|
6909
7141
|
*/
|
|
6910
7142
|
async function exportStrings(root, dir, options = {}) {
|
|
6911
7143
|
const source = options.source ?? DEFAULT_SOURCE;
|
|
6912
|
-
const walk = await readGame(root, options.features ?? DEFAULT_FEATURES);
|
|
7144
|
+
const walk = await readGame(root, options.features ?? DEFAULT_FEATURES, options.layers ?? {});
|
|
6913
7145
|
const locales = [...walk.byLocale.keys()].toSorted();
|
|
6914
7146
|
if (!locales.includes(source)) throw sourceMissing(source);
|
|
6915
7147
|
const keys = [...walk.owner.keys()].toSorted();
|
|
@@ -7007,7 +7239,7 @@ function reviewEntry(walk, review, exchange, key) {
|
|
|
7007
7239
|
try {
|
|
7008
7240
|
compileMessage(text);
|
|
7009
7241
|
review.changes.push({
|
|
7010
|
-
|
|
7242
|
+
folder: owner.folder,
|
|
7011
7243
|
locale: exchange.locale,
|
|
7012
7244
|
key,
|
|
7013
7245
|
text
|
|
@@ -7076,19 +7308,20 @@ async function rewriteStrings(file, texts) {
|
|
|
7076
7308
|
await writeFile(file, jsonText(table));
|
|
7077
7309
|
}
|
|
7078
7310
|
/**
|
|
7079
|
-
* Groups the changes of an import by the string file they land in
|
|
7311
|
+
* Groups the changes of an import by the string file they land in: the `strings/` of the feature
|
|
7312
|
+
* or layer folder that owns each key.
|
|
7080
7313
|
*
|
|
7081
7314
|
* @param changes - Every text the import writes.
|
|
7082
|
-
* @param
|
|
7083
|
-
* @returns The path and the new texts of each file, by its path relative to the root.
|
|
7315
|
+
* @param root - Game source root.
|
|
7316
|
+
* @returns The path and the new texts of each file, by its POSIX path relative to the root.
|
|
7084
7317
|
*/
|
|
7085
|
-
function groupByFile(changes,
|
|
7318
|
+
function groupByFile(changes, root) {
|
|
7086
7319
|
const files = /* @__PURE__ */ new Map();
|
|
7087
7320
|
for (const change of changes) {
|
|
7088
7321
|
const name = `${change.locale}${JSON_EXTENSION}`;
|
|
7089
|
-
const relative =
|
|
7322
|
+
const relative = `${change.folder}/strings/${name}`;
|
|
7090
7323
|
const group = files.get(relative) ?? {
|
|
7091
|
-
file: path.join(
|
|
7324
|
+
file: path.join(root, ...change.folder.split("/"), "strings", name),
|
|
7092
7325
|
texts: /* @__PURE__ */ new Map()
|
|
7093
7326
|
};
|
|
7094
7327
|
group.texts.set(change.key, change.text);
|
|
@@ -7098,32 +7331,39 @@ function groupByFile(changes, folder) {
|
|
|
7098
7331
|
}
|
|
7099
7332
|
/**
|
|
7100
7333
|
* Takes translated exchange files back into the game: every `<dir>/<locale>.json` is checked
|
|
7101
|
-
* against the game first, then each new text is written into the string file of the feature
|
|
7102
|
-
* owns its key, and the strings are compiled into `out` as a dev run does. An empty text
|
|
7103
|
-
* text equal to the current message are skipped. With any problem nothing is written.
|
|
7334
|
+
* against the game first, then each new text is written into the string file of the feature or
|
|
7335
|
+
* layer that owns its key, and the strings are compiled into `out` as a dev run does. An empty text
|
|
7336
|
+
* and a text equal to the current message are skipped. With any problem nothing is written.
|
|
7104
7337
|
*
|
|
7105
7338
|
* @param root - Game source root, the folder that holds the features.
|
|
7106
7339
|
* @param dir - The folder the translators handed back.
|
|
7107
|
-
* @param options - The features folder, where the compile writes, and whether it
|
|
7340
|
+
* @param options - The features folder, the layers, where the compile writes, and whether it
|
|
7341
|
+
* writes `en-XA`.
|
|
7108
7342
|
* @returns The locales read, how many keys were written and the files rewritten.
|
|
7109
|
-
* @throws {Error} One error that lists every problem of the exchange files,
|
|
7110
|
-
*
|
|
7343
|
+
* @throws {Error} One error that lists every problem of the exchange files, the layer error
|
|
7344
|
+
* before anything is read when a layer folder is not one folder name, or the compile's own error
|
|
7345
|
+
* after the files are written (a parameter kind that differs across locales).
|
|
7111
7346
|
* @example
|
|
7112
7347
|
* ```ts
|
|
7113
7348
|
* // The translators handed back translations/ru.json with two hud texts filled in.
|
|
7114
7349
|
* await importStrings("src", "translations", { out: "src/generated" });
|
|
7115
7350
|
* // { locales: ["ru"], keys: 2, files: ["features/hud/strings/ru.json"] }
|
|
7351
|
+
* // A v15 game: the translated ui.ok lands in the layer's own file.
|
|
7352
|
+
* await importStrings("src", "translations", { out: "src/generated", layers: { shared: "ui" } });
|
|
7353
|
+
* // { locales: ["ru"], keys: 1, files: ["shared/strings/ru.json"] }
|
|
7116
7354
|
* ```
|
|
7117
7355
|
*/
|
|
7118
7356
|
async function importStrings(root, dir, options = {}) {
|
|
7119
7357
|
const features = options.features ?? DEFAULT_FEATURES;
|
|
7120
|
-
const
|
|
7358
|
+
const layers = options.layers ?? {};
|
|
7359
|
+
const review = await reviewImport(await readGame(root, features, layers), dir);
|
|
7121
7360
|
if (review.problems.length > 0) throw importFailure(review.problems);
|
|
7122
|
-
const byFile = groupByFile(review.changes,
|
|
7361
|
+
const byFile = groupByFile(review.changes, root);
|
|
7123
7362
|
for (const group of byFile.values()) await rewriteStrings(group.file, group.texts);
|
|
7124
7363
|
await compileStrings(root, options.out ?? path.join(root, "generated"), {
|
|
7125
7364
|
features,
|
|
7126
|
-
pseudo: options.pseudo === true
|
|
7365
|
+
pseudo: options.pseudo === true,
|
|
7366
|
+
layers
|
|
7127
7367
|
});
|
|
7128
7368
|
return {
|
|
7129
7369
|
locales: review.locales.toSorted(),
|