library/lists/lists.json carried one English label per list, so every surface
that showed a list's name printed English in every locale (reported by upg100,
which worked around it inside the packet's own strings).
- A `lists` catalogue area, library/i18n/locales/<lc>/lists.json: a label and a
definition for each of the five lists, in all 11 locales.
- listLabel(id, t) / listDefinition(id, t) in peopleGroupList.js resolve
lists.<id>.label through the caller's translator (useI18n's t, tStatic, or any
key -> string), accept ids and aliases ('700'), and fall back to the
registry's English. The module still imports no i18n runtime, so it stays pure
and testable in Node.
- The registry's labels are now documented as the English fallback only, and the
doxa list gains the definition its catalogue entry has.
- check:list-prop-unit (24 cases, was 17): the Arabic and German catalogues come
through, a missing translator or a raw key falls back to English, the English
catalogue must equal the registry word for word (a drifted registry label fails
it, naming the list), and every locale must carry both fields for every list.
- documentation/reference-universal-props.md: read names through listLabel.
Headless: list-prop-unit, i18n-parity, i18n-never-translated,
no-free-identifiers and universal-props pass. No consumer calls listLabel yet
(the packet has its own strings), so nothing renders differently until one does.
94 lines
6.8 KiB
JavaScript
94 lines
6.8 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* check-list-prop-unit — the people-group LIST prop (library/lists/), measured WITHOUT a browser.
|
|
*
|
|
* Runs the real library module in Node against a stand-in window/location/fetch:
|
|
* - the ladder: profile-config `list` > ?list= > window.MAP_LIST > 'doxa'; ids and aliases ('700');
|
|
* an unknown value falls back to the default;
|
|
* - the default list returns the SAME array, untouched, with no fetch at all;
|
|
* - a list keeps exactly the rows whose slug is in its membership file, fetched from the asset pool;
|
|
* - a membership that cannot load FAILS CLOSED: no rows, never the whole DOXA list;
|
|
* - every registry list with an imb filter has a membership file, and its counts match the slugs;
|
|
* - listLabel / listDefinition read the reader's catalogue (library/i18n/locales/<lc>/lists.json),
|
|
* fall back to the registry's English, the English catalogue equals the registry, and every
|
|
* locale carries a label and a definition for every list.
|
|
* USAGE node internal/check-list-prop-unit.mjs EXIT 0 pass · 1 fail
|
|
*/
|
|
import { register } from 'node:module'
|
|
import { readFileSync, existsSync, readdirSync } from 'node:fs'
|
|
import { resolve, dirname, join } from 'node:path'
|
|
import { fileURLToPath } from 'node:url'
|
|
|
|
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
|
|
// Vite imports JSON without an attribute; Node 20 does not. Teach this process the same thing.
|
|
register('data:text/javascript,' + encodeURIComponent(
|
|
"export async function load(url, ctx, next) { if (url.endsWith('.json')) return { format: 'json', shortCircuit: true, source: (await import('node:fs')).readFileSync(new URL(url)) }; return next(url, ctx) }"))
|
|
|
|
globalThis.window = globalThis
|
|
let search = ''
|
|
Object.defineProperty(globalThis, 'location', { configurable: true, get: () => ({ search, href: 'https://host.example/page' + search }) })
|
|
const fetched = []
|
|
let serve = () => ({ ok: false, status: 404 })
|
|
globalThis.fetch = async (url) => { fetched.push(String(url)); const r = serve(String(url)); return { ok: r.ok, status: r.status, json: async () => r.body } }
|
|
|
|
const L = await import('../library/lists/peopleGroupList.js')
|
|
const results = []
|
|
const check = (name, got, want) => {
|
|
const ok = JSON.stringify(got) === JSON.stringify(want)
|
|
results.push(ok)
|
|
console.log(` ${ok ? 'ok ' : 'FAIL'} ${name.padEnd(66)} ${JSON.stringify(got)}${ok ? '' : ' want ' + JSON.stringify(want)}`)
|
|
}
|
|
const reset = () => { search = ''; delete globalThis.MAP_LIST; fetched.length = 0 }
|
|
|
|
console.log('ladder')
|
|
reset(); check('nothing set -> the default', L.resolvePeopleGroupList(null), 'doxa')
|
|
reset(); check('profile-config list wins', (search = '?list=unreached', globalThis.MAP_LIST = 'frontier', L.resolvePeopleGroupList({ list: 'frontier-500k' })), 'frontier-500k')
|
|
reset(); check('?list= beats window.MAP_LIST', (search = '?list=frontier', globalThis.MAP_LIST = 'unreached', L.resolvePeopleGroupList({})), 'frontier')
|
|
reset(); check('window.MAP_LIST when nothing else', (globalThis.MAP_LIST = 'unreached', L.resolvePeopleGroupList(null)), 'unreached')
|
|
reset(); check('an alias: 700', L.resolvePeopleGroupList({ list: '700' }), 'frontier-500k')
|
|
reset(); check('an alias, any case: Project-7000', L.resolvePeopleGroupList({ list: 'Project-7000' }), 'unreached')
|
|
reset(); check('a JSON-string profile-config', L.resolvePeopleGroupList('{"list":"4700"}'), 'frontier')
|
|
reset(); check('a ref-shaped profile-config', L.resolvePeopleGroupList({ value: { list: '2000' } }), 'doxa')
|
|
reset(); check('unknown -> the default', L.resolvePeopleGroupList({ list: 'project-9999' }), 'doxa')
|
|
|
|
console.log('apply')
|
|
const rows = [{ slug: 'a' }, { slug: 'b' }, { slug: 'c' }]
|
|
reset(); { const out = await L.applyPeopleGroupList(rows, null); check('default: the same array, no fetch', [out === rows, fetched.length], [true, 0]) }
|
|
reset(); serve = (u) => (u.endsWith('/people-groups/lists/frontier.json') ? { ok: true, status: 200, body: { slugs: ['a', 'c'] } } : { ok: false, status: 404 })
|
|
{ const out = await L.applyPeopleGroupList(rows, { list: 'frontier', assetsBase: 'https://cdn.example/tree/assets' }); check('a list keeps exactly its members', out.map((r) => r.slug), ['a', 'c']) }
|
|
check('…fetched from the asset pool', fetched, ['https://cdn.example/tree/assets/people-groups/lists/frontier.json'])
|
|
reset(); serve = () => ({ ok: false, status: 404 })
|
|
{ const err = console.error; console.error = () => {}; const out = await L.applyPeopleGroupList(rows, { list: 'unreached', assetsBase: 'https://cdn.example/x/assets' }); console.error = err; check('an unloadable list FAILS CLOSED: no rows', out, []) }
|
|
|
|
console.log('membership files')
|
|
for (const l of L.peopleGroupLists().filter((x) => x.source === 'imb')) {
|
|
const f = join(ROOT, 'data/people-groups/lists', `${l.id}.json`)
|
|
if (!existsSync(f)) { check(`${l.id}: membership file exists`, false, true); continue }
|
|
const d = JSON.parse(readFileSync(f, 'utf8'))
|
|
check(`${l.id}: file matches its registry filter, counts match its slugs`, [JSON.stringify(d.filter) === JSON.stringify(l.filter), d.counts.doxa === d.slugs.length, d.counts.imb >= d.counts.doxa], [true, true, true])
|
|
}
|
|
|
|
console.log('labels in the reader\'s language')
|
|
const cat = (lc) => JSON.parse(readFileSync(join(ROOT, 'library/i18n/locales', lc, 'lists.json'), 'utf8'))
|
|
const tFrom = (lc) => (key) => key.split('.').slice(1).reduce((o, k) => (o == null ? o : o[k]), cat(lc)) ?? key
|
|
check('no translator -> the registry English', L.listLabel('700'), 'Project 700: frontier people groups of 500,000 or more')
|
|
check('a translator that returns the key -> English', L.listLabel('frontier', (k) => k), 'Project 4700: frontier people groups')
|
|
check('Arabic catalogue, by alias', L.listLabel('4700', tFrom('ar')), cat('ar').frontier.label)
|
|
check('definition, German catalogue', L.listDefinition('unreached', tFrom('de')), cat('de').unreached.definition)
|
|
check('an unknown list -> empty, not a crash', L.listLabel('project-9999', tFrom('en')), '')
|
|
{ const en = cat('en'), bad = []
|
|
for (const l of L.peopleGroupLists()) {
|
|
if (en[l.id]?.label !== l.label) bad.push(`${l.id}.label`)
|
|
if ((en[l.id]?.definition || '') !== (l.definition || '')) bad.push(`${l.id}.definition`)
|
|
}
|
|
check('the English catalogue matches the registry, word for word', bad, []) }
|
|
{ const missing = []
|
|
for (const lc of readdirSync(join(ROOT, 'library/i18n/locales'))) {
|
|
const c = cat(lc)
|
|
for (const l of L.peopleGroupLists()) for (const f of ['label', 'definition']) if (!c[l.id]?.[f]) missing.push(`${lc}:${l.id}.${f}`)
|
|
}
|
|
check('every locale has a label and a definition for every list', missing, []) }
|
|
|
|
const failed = results.filter((x) => !x).length
|
|
console.log(failed ? `LIST-PROP: FAIL — ${failed} case(s)` : `LIST-PROP: PASS — ${results.length} cases: the ladder, aliases, the untouched default, membership filtering, fail-closed, the membership files, and labels in every locale`)
|
|
process.exit(failed ? 1 : 0)
|