141 lines
5.6 KiB
JavaScript
141 lines
5.6 KiB
JavaScript
// Guards the content packs: every theme and language must be complete, so a
|
|
// missing translation is caught by `npm test` instead of showing a raw key.
|
|
|
|
import assert from 'node:assert/strict';
|
|
import { readdirSync, readFileSync } from 'node:fs';
|
|
import { test } from 'node:test';
|
|
import { flatKeys, loadContext, PUBLIC_DIR, readJson } from './helpers.js';
|
|
|
|
const ctx = loadContext();
|
|
const codes = (dir) =>
|
|
readdirSync(PUBLIC_DIR + dir)
|
|
.filter((f) => f.endsWith('.json'))
|
|
.map((f) => f.slice(0, -5));
|
|
const themes = readdirSync(PUBLIC_DIR + 'themes', { withFileTypes: true })
|
|
.filter((e) => e.isDirectory())
|
|
.map((e) => e.name);
|
|
|
|
test('core locales define the same keys as English', () => {
|
|
const reference = flatKeys(readJson('locales/en.json')).sort();
|
|
for (const code of codes('locales')) {
|
|
assert.deepEqual(flatKeys(readJson(`locales/${code}.json`)).sort(), reference, `locales/${code}.json`);
|
|
}
|
|
});
|
|
|
|
test('every effect type has a core text', () => {
|
|
const keys = new Set(flatKeys(readJson('locales/en.json')));
|
|
for (const u of ctx.upgrades) assert.ok(keys.has(`effect.${u.effect.type}`), u.effect.type);
|
|
});
|
|
|
|
test('there is at least one theme, each with a manifest, a stylesheet and English texts', () => {
|
|
assert.ok(themes.length >= 1);
|
|
for (const id of themes) {
|
|
assert.ok(readdirSync(`${PUBLIC_DIR}themes/${id}`).includes('theme.css'), `${id}/theme.css`);
|
|
assert.ok(codes(`themes/${id}/locales`).includes('en'), `${id} needs an en locale`);
|
|
assert.ok(readJson(`themes/${id}/theme.json`));
|
|
}
|
|
});
|
|
|
|
test('every theme names every resource, generator and upgrade, in every language', () => {
|
|
const required = [
|
|
'theme.name',
|
|
'theme.title',
|
|
'theme.res.units',
|
|
'theme.res.unit',
|
|
'theme.res.funds',
|
|
'theme.res.material',
|
|
'theme.fmt.funds',
|
|
'theme.action.make',
|
|
'theme.action.buyMaterial',
|
|
...ctx.generators.flatMap((g) => [`theme.gen.${g.id}.name`, `theme.gen.${g.id}.desc`]),
|
|
...ctx.upgrades.map((u) => `theme.upg.${u.id}`),
|
|
'theme.tab.gamble',
|
|
'theme.action.gamble',
|
|
'theme.res.chips',
|
|
'theme.res.research',
|
|
'theme.res.legacy',
|
|
...ctx.investments.societies.map((s) => `theme.inv.${s.id}.name`),
|
|
];
|
|
|
|
for (const id of themes) {
|
|
const reference = flatKeys(readJson(`themes/${id}/locales/en.json`)).sort();
|
|
assert.deepEqual(reference.filter((k) => required.includes(k)).sort(), [...required].sort(), `${id}/en`);
|
|
for (const code of codes(`themes/${id}/locales`)) {
|
|
const keys = flatKeys(readJson(`themes/${id}/locales/${code}.json`)).sort();
|
|
assert.deepEqual(keys, reference, `${id}/${code} differs from ${id}/en`);
|
|
}
|
|
}
|
|
});
|
|
|
|
test('every theme is translated in every core language', () => {
|
|
for (const id of themes) {
|
|
for (const code of codes('locales')) {
|
|
assert.ok(codes(`themes/${id}/locales`).includes(code), `${id} has no "${code}" texts`);
|
|
}
|
|
}
|
|
});
|
|
|
|
// ------------------------------------------------- the new systems' vocabulary
|
|
|
|
const coreKeys = new Set(flatKeys(readJson('locales/en.json')));
|
|
const coreHas = (key) => coreKeys.has(key);
|
|
|
|
test('every effect type used anywhere has a text', () => {
|
|
const effects = [
|
|
...ctx.upgrades.map((u) => u.effect),
|
|
...ctx.research.map((r) => r.effect),
|
|
...ctx.legacy.map((p) => p.effect),
|
|
...ctx.achievements.map((a) => a.effect),
|
|
];
|
|
for (const e of effects) assert.ok(coreHas(`effect.${e.type}`), `effect.${e.type}`);
|
|
});
|
|
|
|
test('research, perks, achievements, events, contracts and tiers all have core texts', () => {
|
|
const need = [
|
|
...ctx.research.map((r) => `research.node.${r.id}`),
|
|
...[...new Set(ctx.research.map((r) => r.branch))].map((b) => `research.branch.${b}`),
|
|
...ctx.legacy.map((p) => `legacy.perk.${p.id}`),
|
|
...ctx.achievements.map((a) => `ach.title.${a.id}`),
|
|
...[...new Set(ctx.achievements.map((a) => a.condition.stat))].map((s) => `ach.cond.${s}`),
|
|
...ctx.rules.events.types.flatMap((e) => [`event.${e.id}.title`, `event.${e.id}.text`]),
|
|
...ctx.rules.contracts.templates.map((c) => `contracts.template.${c.id}`),
|
|
...ctx.rules.contracts.rewards.map((r) => `contracts.reward.${r}`),
|
|
...Object.keys(ctx.investments.tiers).map((tier) => `invest.tier.${tier}`),
|
|
...['win', 'jackpot', 'lose', 'crash'].map((o) => `gamble.result.${o}`),
|
|
];
|
|
for (const key of need) assert.ok(coreHas(key), key);
|
|
});
|
|
|
|
test('every section has a label (core, or themed for the gamble)', () => {
|
|
for (const tab of ctx.tabs) {
|
|
assert.ok(coreHas(`tab.${tab.id}`) || tab.id === 'gamble', `tab.${tab.id}`);
|
|
}
|
|
});
|
|
|
|
test('every key used by the code and the page exists', () => {
|
|
const themeKeys = new Set(flatKeys(readJson('themes/paperclip/locales/en.json')));
|
|
const known = (key) => coreKeys.has(key) || themeKeys.has(key);
|
|
|
|
const used = new Set();
|
|
const walk = (dir) => {
|
|
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
const path = `${dir}/${entry.name}`;
|
|
if (entry.isDirectory()) walk(path);
|
|
else if (entry.name.endsWith('.js')) {
|
|
for (const m of readFileSync(path, 'utf8').matchAll(/\bt\(\s*'([\w.]+)'/g)) used.add(m[1]);
|
|
}
|
|
}
|
|
};
|
|
walk(`${PUBLIC_DIR}js`);
|
|
|
|
const html = readFileSync(`${PUBLIC_DIR}index.html`, 'utf8');
|
|
for (const m of html.matchAll(/data-i18n="([\w.]+)"/g)) used.add(m[1]);
|
|
for (const m of html.matchAll(/data-i18n-attr="([^"]+)"/g)) {
|
|
for (const pair of m[1].split(';')) used.add(pair.split(':')[1].trim());
|
|
}
|
|
|
|
const missing = [...used].filter((key) => !known(key));
|
|
assert.deepEqual(missing, [], `texts used but not defined: ${missing.join(', ')}`);
|
|
assert.ok(used.size > 100, 'the scan found the texts');
|
|
});
|