mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-20 06:33:38 +02:00
Citycorners-Reste vom vorherigen Sprint mit committet. food → Nutriphi,
wardrobe → Werdrobe sind als Standalone-Apps live; die mana.how-unified-
App trägt die Modul-Surfaces nicht mehr.
Gelöscht / abgebaut:
- Module: apps/mana/.../modules/{food,wardrobe} + Routen + Locales
- Landing-Apps: apps/{food,citycorners}/ Top-Level
- Backend: apps/api/src/modules/{food,wardrobe} + MCP-Tools log_meal /
nutrition_summary, picture-routes verifyMediaOwnership-Allowlist
- shared-branding: APP_BRANDING, APP_ICONS, MANA_APPS, Logos, Onboarding
- shared-ai, mana-tool-registry, credits, shared-types/spaces,
shared-utils/analytics, spiral-db/MANA_APP_INDEX, website-blocks
- Cross-Module: Body-CalorieWeightChart, Comic-CharacterPicker-Wardrobe,
website-Embed wardrobe.outfits, DaySnapshot.nutrition, FoodEventType,
MealLogged/Meal*-Streaks/Goals/Companion/Trigger, AI-Agent-Policy,
GoalEditor MealLogged, MyDay/RitualRunner/Rules nutrition-Refs,
Crypto-Registry meals/wardrobeGarments/wardrobeOutfits
- Generic: PlaceCategory 'food' (places + geocoding + Locales),
spaces.ts 'food'/'wardrobe' Modul-IDs
- Infrastruktur: cloudflared, docker-compose CORS, nginx-Landing,
prometheus-Probe, load-tests, package.json dev-Scripts,
generate-env, mac-mini/build-landings, dependabot
Dexie v62 Migration:
- droppt meals, goals, foodFavorites, mealTags, wardrobeGarments,
wardrobeOutfits Tabellen
- entfernt wardrobeOutfitId / wardrobeGarmentId aus images-Index
- Upgrade-Callback strippt die toten FK-Properties aus alten image-Rows
Test/Doku:
- module-registry.test.ts: Snapshot refresht auf aktuellen Stand mit
56 Modulen (vorher 32, statisch eingefroren pre-refactor). Plus
LEGACY_TABLES-Exclusion für nicht-mehr-registrierte Tabellen aus
cards/citycorners/moodlit/rituals/wishes/who.
- streaks.test.ts: MealLogged-Test in TaskCompleted-Test umgebaut
- apps/mana/CLAUDE.md: food-Refs in AI-Tool-Tabelle und
AiProposalInbox-Liste entfernt
- validate-i18n-keys.mjs + validate-no-recursive-turbo.mjs:
existsSync-Guard, damit die Skripte mit gestaged-aber-rm'ten Dateien
klarkommen
mana-web svelte-check 0 errors / 7436 files, betroffene Tests grün
(streaks, dashboard, module-registry), validate:pg-schema,
validate:turbo, validate:i18n-parity grün.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
215 lines
7.3 KiB
JavaScript
215 lines
7.3 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* Cross-checks i18n key usage in code against keys defined in DE
|
|
* locale JSONs. Two directions:
|
|
*
|
|
* - **Missing**: a `$_('a.b.c')` call where `a.b.c` is not in DE.
|
|
* These would render as the raw key string at runtime — a
|
|
* user-visible bug. Tracked against a per-file baseline so the
|
|
* existing backlog doesn't block CI but new misses fail hard.
|
|
*
|
|
* - **Dead**: a key in DE that no `$_(…)` call references (statically
|
|
* or via a known dynamic prefix). Reported as INFO; not enforced
|
|
* because the writing-key-first workflow would otherwise block.
|
|
*
|
|
* Dynamic suffixes via template literals (`$_(`ns.foo.${x}`)`) and
|
|
* concatenations (`$_('ns.foo.' + x)`) become "prefix masks": every
|
|
* key under `ns.foo.` is treated as potentially used.
|
|
*
|
|
* Usage:
|
|
* node scripts/validate-i18n-keys.mjs # check against baseline
|
|
* node scripts/validate-i18n-keys.mjs --update # rewrite baseline
|
|
* node scripts/validate-i18n-keys.mjs --report # print full dead-key list
|
|
*/
|
|
|
|
import { readFileSync, writeFileSync, readdirSync, statSync, existsSync } from 'node:fs';
|
|
import { fileURLToPath } from 'node:url';
|
|
import { dirname, join } from 'node:path';
|
|
import { execSync } from 'node:child_process';
|
|
|
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
const REPO_ROOT = join(__dirname, '..');
|
|
const LOCALES_DIR = join(REPO_ROOT, 'apps/mana/apps/web/src/lib/i18n/locales');
|
|
const SRC_DIR = 'apps/mana/apps/web/src';
|
|
const BASELINE_PATH = join(__dirname, 'i18n-missing-baseline.json');
|
|
|
|
function flattenKeys(obj, prefix = '') {
|
|
const keys = [];
|
|
for (const [k, v] of Object.entries(obj)) {
|
|
const path = prefix ? `${prefix}.${k}` : k;
|
|
if (v && typeof v === 'object' && !Array.isArray(v)) keys.push(...flattenKeys(v, path));
|
|
else keys.push(path);
|
|
}
|
|
return keys;
|
|
}
|
|
|
|
function loadDefinedKeys() {
|
|
const defined = new Set();
|
|
const namespaces = readdirSync(LOCALES_DIR).filter((f) =>
|
|
statSync(join(LOCALES_DIR, f)).isDirectory()
|
|
);
|
|
for (const ns of namespaces) {
|
|
const path = join(LOCALES_DIR, ns, 'de.json');
|
|
if (!existsSync(path)) continue;
|
|
for (const k of flattenKeys(JSON.parse(readFileSync(path, 'utf8')))) {
|
|
defined.add(`${ns}.${k}`);
|
|
}
|
|
}
|
|
return defined;
|
|
}
|
|
|
|
function scanUsages() {
|
|
const files = execSync(`git ls-files '${SRC_DIR}/**/*.svelte' '${SRC_DIR}/**/*.ts'`, {
|
|
cwd: REPO_ROOT,
|
|
})
|
|
.toString()
|
|
.trim()
|
|
.split('\n')
|
|
.filter(Boolean);
|
|
|
|
// per-key list of files where it's referenced — for nice error reporting
|
|
const usedByFile = new Map();
|
|
const dynamicPrefixes = new Set();
|
|
|
|
for (const f of files) {
|
|
const abs = join(REPO_ROOT, f);
|
|
if (!existsSync(abs)) continue; // tracked but deleted-on-disk (rm without commit)
|
|
const src = readFileSync(abs, 'utf8');
|
|
|
|
// $_('a.b.c') or _('a.b.c')
|
|
for (const m of src.matchAll(/\$?_\(\s*['"]([a-zA-Z][\w.-]*)['"]/g)) {
|
|
const key = m[1];
|
|
if (!usedByFile.has(key)) usedByFile.set(key, new Set());
|
|
usedByFile.get(key).add(f);
|
|
}
|
|
|
|
// $_(`a.b.${x}`) → prefix "a.b."
|
|
for (const m of src.matchAll(/\$?_\(\s*`([a-zA-Z][\w.-]*\.)\$\{/g)) {
|
|
dynamicPrefixes.add(m[1]);
|
|
}
|
|
|
|
// $_('a.b.' + x) → prefix "a.b."
|
|
for (const m of src.matchAll(/\$?_\(\s*['"]([a-zA-Z][\w.-]*\.)['"]\s*\+/g)) {
|
|
dynamicPrefixes.add(m[1]);
|
|
}
|
|
}
|
|
|
|
return { usedByFile, dynamicPrefixes };
|
|
}
|
|
|
|
function loadBaseline() {
|
|
if (!existsSync(BASELINE_PATH)) return {};
|
|
return JSON.parse(readFileSync(BASELINE_PATH, 'utf8'));
|
|
}
|
|
|
|
function buildPerFileMissing(usedByFile, defined) {
|
|
// Returns: { file: { count, keys: [...] } }
|
|
const perFile = {};
|
|
for (const [key, files] of usedByFile) {
|
|
if (defined.has(key)) continue;
|
|
for (const f of files) {
|
|
if (!perFile[f]) perFile[f] = { count: 0, keys: new Set() };
|
|
perFile[f].count++;
|
|
perFile[f].keys.add(key);
|
|
}
|
|
}
|
|
const result = {};
|
|
for (const [f, { count, keys }] of Object.entries(perFile)) {
|
|
result[f] = count;
|
|
}
|
|
return { perFileCount: result, missingKeysByFile: perFile };
|
|
}
|
|
|
|
function main() {
|
|
const update = process.argv.includes('--update');
|
|
const reportMode = process.argv.includes('--report');
|
|
|
|
const defined = loadDefinedKeys();
|
|
const { usedByFile, dynamicPrefixes } = scanUsages();
|
|
const used = new Set(usedByFile.keys());
|
|
|
|
const dead = [...defined].filter(
|
|
(k) => !used.has(k) && ![...dynamicPrefixes].some((p) => k.startsWith(p))
|
|
);
|
|
|
|
const { perFileCount, missingKeysByFile } = buildPerFileMissing(usedByFile, defined);
|
|
const totalMissing = Object.values(perFileCount).reduce((a, b) => a + b, 0);
|
|
|
|
if (reportMode) {
|
|
console.log(`Defined keys: ${defined.size}`);
|
|
console.log(`Statically used: ${used.size}, dynamic prefixes: ${dynamicPrefixes.size}`);
|
|
console.log(`Dead keys (defined, never referenced): ${dead.length}`);
|
|
console.log('\n--- top 30 dead keys ---');
|
|
for (const k of dead.slice(0, 30)) console.log(' ' + k);
|
|
console.log('\n--- missing keys (used, undefined) ---');
|
|
for (const [f, info] of Object.entries(missingKeysByFile).slice(0, 20)) {
|
|
console.log(` ${f}: ${info.count}`);
|
|
for (const k of [...info.keys].slice(0, 5)) console.log(` - ${k}`);
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (update) {
|
|
const sorted = Object.fromEntries(
|
|
Object.entries(perFileCount).sort(([a], [b]) => a.localeCompare(b))
|
|
);
|
|
writeFileSync(BASELINE_PATH, JSON.stringify(sorted, null, 2) + '\n');
|
|
console.log(
|
|
`✓ Baseline updated: ${totalMissing} missing-key reference(s) across ${Object.keys(perFileCount).length} files.`
|
|
);
|
|
return;
|
|
}
|
|
|
|
const baseline = loadBaseline();
|
|
const baselineTotal = Object.values(baseline).reduce((a, b) => a + b, 0);
|
|
const violations = [];
|
|
for (const [file, n] of Object.entries(perFileCount)) {
|
|
const b = baseline[file] ?? 0;
|
|
if (n > b) {
|
|
violations.push({
|
|
file,
|
|
current: n,
|
|
baseline: b,
|
|
delta: n - b,
|
|
keys: [...missingKeysByFile[file].keys].filter(
|
|
(k) => !(baseline[file] && false) // we don't track which exact keys were baselined; show all
|
|
),
|
|
});
|
|
}
|
|
}
|
|
|
|
if (violations.length > 0) {
|
|
console.error(
|
|
`\n✗ i18n missing-key check FAILED — ${violations.length} file(s) over baseline:\n`
|
|
);
|
|
for (const v of violations.slice(0, 20)) {
|
|
console.error(` ${v.file}: ${v.current} (was ${v.baseline}, +${v.delta})`);
|
|
for (const k of v.keys.slice(0, 3)) console.error(` - ${k}`);
|
|
if (v.keys.length > 3) console.error(` … +${v.keys.length - 3} more keys`);
|
|
}
|
|
if (violations.length > 20) console.error(` … +${violations.length - 20} more files`);
|
|
console.error(
|
|
`\nA $_('…') call references a key that does not exist in any DE locale.\n` +
|
|
`At runtime this renders as the raw key string. Add the key to the\n` +
|
|
`appropriate locales/<ns>/de.json (parity validator will demand the\n` +
|
|
`other locales) — or fix the typo.\n` +
|
|
`If intentional (e.g. you renamed away a key still being referenced\n` +
|
|
`in legacy code), run: pnpm run validate:i18n-keys -- --update\n`
|
|
);
|
|
process.exit(1);
|
|
}
|
|
|
|
const shrunk = Object.keys(baseline).filter((f) => (perFileCount[f] ?? 0) < baseline[f]).length;
|
|
const cleaned = Object.keys(baseline).filter((f) => !(f in perFileCount)).length;
|
|
|
|
console.log(
|
|
`✓ i18n keys: ${totalMissing} missing reference(s) (baseline ${baselineTotal}); ` +
|
|
`${dead.length} dead key(s) defined but unused.` +
|
|
(shrunk || cleaned
|
|
? `\n ${shrunk} file(s) shrunk, ${cleaned} file(s) fully cleaned — ` +
|
|
`run 'pnpm run validate:i18n-keys -- --update' to ratchet.`
|
|
: '')
|
|
);
|
|
}
|
|
|
|
main();
|