feat: First iteration, multi themes, I18N
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
import { readdir, readFile } from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
|
||||
// Content packs are discovered from the filesystem, so adding a theme or a
|
||||
// language is just dropping files in a folder: no code change, no rebuild list.
|
||||
|
||||
const THEME_ID = /^[a-z0-9][a-z0-9-]*$/;
|
||||
const LOCALE_CODE = /^[a-z]{2,3}(-[A-Za-z0-9]+)*$/;
|
||||
|
||||
async function readJson(file) {
|
||||
return JSON.parse(await readFile(file, 'utf8'));
|
||||
}
|
||||
|
||||
async function listLocaleCodes(dir) {
|
||||
let names;
|
||||
try {
|
||||
names = await readdir(dir);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
return names
|
||||
.filter((n) => n.endsWith('.json'))
|
||||
.map((n) => n.slice(0, -'.json'.length))
|
||||
.filter((code) => LOCALE_CODE.test(code))
|
||||
.sort();
|
||||
}
|
||||
|
||||
/** GET /api/themes -> [{ id, order, assets, locales }] */
|
||||
async function listThemes(publicDir) {
|
||||
const root = path.join(publicDir, 'themes');
|
||||
const entries = await readdir(root, { withFileTypes: true }).catch(() => []);
|
||||
const themes = [];
|
||||
for (const entry of entries) {
|
||||
if (!entry.isDirectory() || !THEME_ID.test(entry.name)) continue;
|
||||
const dir = path.join(root, entry.name);
|
||||
try {
|
||||
const manifest = await readJson(path.join(dir, 'theme.json'));
|
||||
themes.push({
|
||||
id: entry.name,
|
||||
order: Number.isFinite(manifest.order) ? manifest.order : 100,
|
||||
assets: manifest.assets && typeof manifest.assets === 'object' ? manifest.assets : {},
|
||||
locales: await listLocaleCodes(path.join(dir, 'locales')),
|
||||
});
|
||||
} catch {
|
||||
// Skip folders without a valid theme.json rather than failing the whole list.
|
||||
}
|
||||
}
|
||||
return themes.sort((a, b) => a.order - b.order || a.id.localeCompare(b.id));
|
||||
}
|
||||
|
||||
/** GET /api/locales -> [{ code, name }] */
|
||||
async function listLocales(publicDir) {
|
||||
const dir = path.join(publicDir, 'locales');
|
||||
const out = [];
|
||||
for (const code of await listLocaleCodes(dir)) {
|
||||
try {
|
||||
const file = await readJson(path.join(dir, `${code}.json`));
|
||||
out.push({ code, name: file?.meta?.name ?? code });
|
||||
} catch {
|
||||
// Ignore malformed locale files.
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/** Returns a JSON payload for known /api routes, or undefined for unknown ones. */
|
||||
export async function handleApi(publicDir, pathname) {
|
||||
switch (pathname) {
|
||||
case '/api/themes':
|
||||
return listThemes(publicDir);
|
||||
case '/api/locales':
|
||||
return listLocales(publicDir);
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user