This commit is contained in:
Andras Schmelczer 2026-06-25 22:08:36 +01:00
parent c2c4ed6708
commit 2efa4d9f47
9 changed files with 1324 additions and 125 deletions

View file

@ -214,16 +214,41 @@ function readServerFeatureNames() {
return [...new Set(names)];
}
// Names of the Enum/Numeric feature *configs* (each needs a description + detail
// translation). We take the FIRST `name:` field after every Feature::Enum( /
// Feature::Numeric( opening. This deliberately skips macro-generated configs
// whose name is a `concat!(...)` expression (the crime rates — handled via
// deriveLegacyCrimeKeys instead) and stops the lazy match from running past such
// a config into an unrelated FeatureGroup `name:` (which previously made the
// group name "Properties" look like a required feature).
function readServerFeatureConfigNames() {
const src = readFileSync(FEATURES_RS, 'utf8');
const names = [];
const re = /Feature::(?:Enum|Numeric)\([^]*?name:\s*"((?:\\.|[^"\\])*)"/g;
for (const match of src.matchAll(re)) {
names.push(JSON.parse(`"${match[1]}"`));
const re = /Feature::(?:Enum|Numeric)\(/g;
let m;
while ((m = re.exec(src))) {
const after = src.slice(m.index + m[0].length);
const nm = after.match(/\bname:\s*(?:"((?:\\.|[^"\\])*)"|concat!)/);
if (nm && nm[1] !== undefined) names.push(JSON.parse(`"${nm[1]}"`));
}
return [...new Set(names)];
}
// The crime-rate features are generated by the crime_rate_features! macro and
// named "<type> (/yr, 7y|2y)". Their description/detail translations are shared
// across both windows under a single legacy "<type> (avg/yr)" key. This mirror
// of legacyCrimeFeatureKey() in descriptions.ts MUST use the same suffix regex,
// derived straight from the server keys so the checker and the runtime lookup
// stay in lock-step.
function deriveLegacyCrimeKeys(serverKeys) {
const legacy = new Set();
for (const key of serverKeys) {
const match = key.match(/^(.*) \(\/yr, \d+y\)$/);
if (match) legacy.add(`${match[1]} (avg/yr)`);
}
return legacy;
}
function readNamedRecord(file, varName) {
const sf = parseFile(join(I18N_DIR, file));
const init = findVarInitializer(sf, varName);
@ -355,7 +380,7 @@ function checkLocales(supportedCodes) {
}
}
function checkRecordCoverage(file, varName, supportedCodes, serverKeys, requiredKeys) {
function checkRecordCoverage(file, varName, supportedCodes, serverKeys, requiredKeys, legacyCrimeKeys) {
const record = readNamedRecord(file, varName);
const expected = supportedCodes.filter((c) => c !== 'en');
const present = Object.keys(record);
@ -397,10 +422,12 @@ function checkRecordCoverage(file, varName, supportedCodes, serverKeys, required
}
}
// Every key here must also be a translatable feature name in en.ts > server.
// Otherwise the description is unreachable — ts() looks up server.${name}.
// Every key here must also be a translatable feature name in en.ts > server,
// or a legacy crime key that maps onto the per-window server keys (see
// deriveLegacyCrimeKeys / legacyCrimeFeatureKey). Otherwise the description is
// unreachable — ts() looks up server.${name}.
for (const key of union) {
if (!serverKeys.has(key)) {
if (!serverKeys.has(key) && !legacyCrimeKeys.has(key)) {
fail(`${file}: key "${key}" has no matching entry in en.ts > server`);
}
}
@ -478,16 +505,25 @@ function main() {
checkLocaleLoaders(supportedCodes);
const en = readLocale('en');
const serverKeys = new Set(Object.keys(en.server ?? {}));
const sourceFeatureKeys = readServerFeatureConfigNames();
const legacyCrimeKeys = deriveLegacyCrimeKeys(serverKeys);
const requiredKeys = [...readServerFeatureConfigNames(), ...legacyCrimeKeys];
checkServerSourceCoverage(serverKeys);
checkRecordCoverage(
'descriptions.ts',
'descriptions',
supportedCodes,
serverKeys,
sourceFeatureKeys
requiredKeys,
legacyCrimeKeys
);
checkRecordCoverage(
'details.ts',
'details',
supportedCodes,
serverKeys,
requiredKeys,
legacyCrimeKeys
);
checkRecordCoverage('details.ts', 'details', supportedCodes, serverKeys, sourceFeatureKeys);
checkForbiddenVisibleStrings();
for (const w of warnings) console.warn(`warn: ${w}`);