@urbicon-ui/design 6.3.7 → 6.3.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1,8 +1,10 @@
1
1
  #!/usr/bin/env node
2
+ import { createRequire } from "node:module";
3
+ var __require = /* @__PURE__ */ createRequire(import.meta.url);
2
4
 
3
5
  // src/cli/index.ts
4
- import { readFile as readFile10 } from "node:fs/promises";
5
- import { resolve as resolve10 } from "node:path";
6
+ import { readFile as readFile11 } from "node:fs/promises";
7
+ import { resolve as resolve11 } from "node:path";
6
8
 
7
9
  // src/cli/args.ts
8
10
  var BOOLEAN_FLAGS = new Set([
@@ -762,7 +764,7 @@ function extractSection2(content, section) {
762
764
  }
763
765
  // src/cli/content.ts
764
766
  import { readFile as readFile4 } from "node:fs/promises";
765
- import { createRequire } from "node:module";
767
+ import { createRequire as createRequire2 } from "node:module";
766
768
  import { dirname as dirname2, resolve as resolve3 } from "node:path";
767
769
 
768
770
  // ../design-content/src/content-loader.ts
@@ -803,7 +805,7 @@ function ensureContentDir() {
803
805
  if (process.env.URBICON_CONTENT_DIR)
804
806
  return;
805
807
  try {
806
- const require2 = createRequire(import.meta.url);
808
+ const require2 = createRequire2(import.meta.url);
807
809
  const pkgJson = require2.resolve("@urbicon-ui/design-content/package.json");
808
810
  process.env.URBICON_CONTENT_DIR = resolve3(dirname2(pkgJson), "content");
809
811
  } catch {}
@@ -2401,19 +2403,303 @@ Fix the issues above and re-save. — urbicon design gate`);
2401
2403
  return HOOK_BLOCK;
2402
2404
  }
2403
2405
 
2406
+ // src/cli/commands/i18n.ts
2407
+ import { readdir as readdir2, readFile as readFile6, stat as stat2 } from "node:fs/promises";
2408
+ import { basename, join as join3, relative as relative2, resolve as resolve6, sep as sep2 } from "node:path";
2409
+ import { pathToFileURL } from "node:url";
2410
+ var CHECKS = ["parity", "unused", "hardcoded", "audit"];
2411
+ var isCheck = (value) => CHECKS.includes(value);
2412
+ var SKIP_DIRS2 = new Set([
2413
+ "node_modules",
2414
+ ".svelte-kit",
2415
+ ".git",
2416
+ "dist",
2417
+ "build",
2418
+ ".next",
2419
+ ".turbo",
2420
+ "coverage"
2421
+ ]);
2422
+ var MAX_DEPTH2 = 24;
2423
+ var DEFAULT_CONFIG = {
2424
+ sources: ["src"],
2425
+ translations: ["src/lib/translations"]
2426
+ };
2427
+ function label(abs) {
2428
+ return relative2(process.cwd(), abs).split(sep2).join("/") || abs;
2429
+ }
2430
+ function listFlag(flags, key) {
2431
+ const raw = stringFlag(flags, key);
2432
+ if (raw === undefined)
2433
+ return;
2434
+ const items = raw.split(",").map((item) => item.trim()).filter(Boolean);
2435
+ return items.length ? items : undefined;
2436
+ }
2437
+ async function loadConfig(flags, sourceDirs) {
2438
+ let fileConfig = {};
2439
+ const explicit = stringFlag(flags, "config");
2440
+ const candidate = explicit ?? "i18n.audit.json";
2441
+ try {
2442
+ fileConfig = JSON.parse(await readFile6(resolve6(candidate), "utf-8"));
2443
+ } catch (error) {
2444
+ if (explicit)
2445
+ throw new Error(`cannot read config "${explicit}": ${error.message}`);
2446
+ }
2447
+ const merged = { ...DEFAULT_CONFIG, ...fileConfig };
2448
+ if (sourceDirs.length)
2449
+ merged.sources = sourceDirs;
2450
+ merged.functionNames = listFlag(flags, "function-names") ?? merged.functionNames;
2451
+ merged.dynamicKeys = listFlag(flags, "dynamic-keys") ?? merged.dynamicKeys;
2452
+ merged.ignoreKeys = listFlag(flags, "ignore-keys") ?? merged.ignoreKeys;
2453
+ merged.ignoreStrings = listFlag(flags, "ignore-strings") ?? merged.ignoreStrings;
2454
+ merged.baseLocale = stringFlag(flags, "base-locale") ?? merged.baseLocale;
2455
+ merged.translations = listFlag(flags, "translations") ?? merged.translations;
2456
+ merged.runtimeUsage = stringFlag(flags, "runtime-usage") ?? merged.runtimeUsage;
2457
+ return merged;
2458
+ }
2459
+ async function collectFiles(dir, extensions, depth = 0) {
2460
+ if (depth > MAX_DEPTH2)
2461
+ return [];
2462
+ let entries;
2463
+ try {
2464
+ entries = await readdir2(dir, { withFileTypes: true });
2465
+ } catch {
2466
+ return [];
2467
+ }
2468
+ const files = [];
2469
+ for (const entry of entries) {
2470
+ const full = join3(dir, entry.name);
2471
+ if (entry.isDirectory()) {
2472
+ if (SKIP_DIRS2.has(entry.name) || entry.name.startsWith("."))
2473
+ continue;
2474
+ files.push(...await collectFiles(full, extensions, depth + 1));
2475
+ } else if (entry.isFile() && extensions.some((ext) => entry.name.endsWith(ext)) && !/\.(test|spec)\.|\.d\.ts$/.test(entry.name)) {
2476
+ files.push(full);
2477
+ }
2478
+ }
2479
+ return files;
2480
+ }
2481
+ var BUNDLE_EXT = /\.(ts|js|mjs)$/;
2482
+ async function loadBundleGroups(dirs, isSupportedLocale) {
2483
+ const groups = [];
2484
+ const errors = [];
2485
+ for (const dir of dirs) {
2486
+ const abs = resolve6(dir);
2487
+ let candidates;
2488
+ try {
2489
+ const info = await stat2(abs);
2490
+ candidates = info.isDirectory() ? (await readdir2(abs)).filter((f) => BUNDLE_EXT.test(f) && !f.endsWith(".d.ts")).map((f) => join3(abs, f)) : [abs];
2491
+ } catch {
2492
+ errors.push(`translations path not found: ${label(abs)}`);
2493
+ continue;
2494
+ }
2495
+ const bundles = {};
2496
+ let localeFiles = 0;
2497
+ for (const file of candidates) {
2498
+ const locale = basename(file).replace(BUNDLE_EXT, "");
2499
+ if (!isSupportedLocale(locale))
2500
+ continue;
2501
+ localeFiles++;
2502
+ if (bundles[locale]) {
2503
+ errors.push(`duplicate locale "${locale}" in ${label(abs)} — ${label(file)} ignored`);
2504
+ continue;
2505
+ }
2506
+ try {
2507
+ const mod = await import(pathToFileURL(file).href);
2508
+ if (mod.default && typeof mod.default === "object") {
2509
+ bundles[locale] = mod.default;
2510
+ } else {
2511
+ errors.push(`bundle ${label(file)} has no object default export`);
2512
+ }
2513
+ } catch (error) {
2514
+ errors.push(`cannot load bundle ${label(file)}: ${error.message}`);
2515
+ }
2516
+ }
2517
+ if (Object.keys(bundles).length)
2518
+ groups.push({ name: label(abs), bundles });
2519
+ else if (localeFiles === 0)
2520
+ errors.push(`no locale bundles (en/de/…) in: ${label(abs)}`);
2521
+ }
2522
+ return { groups, errors };
2523
+ }
2524
+ function runParity(audit, config, groups) {
2525
+ const lines = [];
2526
+ let errors = 0;
2527
+ let warnings = 0;
2528
+ const allFindings = [];
2529
+ for (const group of groups) {
2530
+ const report = audit.auditTranslations(group.name, group.bundles, {
2531
+ baseLocale: config.baseLocale,
2532
+ ignoreKeys: config.ignoreKeys
2533
+ });
2534
+ errors += report.errors.length;
2535
+ warnings += report.warnings.length;
2536
+ allFindings.push(...report.findings);
2537
+ for (const finding of report.findings) {
2538
+ const icon = finding.severity === "error" ? "✗" : "!";
2539
+ lines.push(` ${icon} [${finding.code}] ${finding.locale} — ${finding.detail}`);
2540
+ }
2541
+ }
2542
+ return { lines, errors, warnings, json: { findings: allFindings } };
2543
+ }
2544
+ async function runUnused(audit, config, groups, sources) {
2545
+ const defined = new Set;
2546
+ for (const group of groups) {
2547
+ const base = group.bundles.en ?? Object.values(group.bundles)[0];
2548
+ if (base)
2549
+ for (const key of audit.collectDeepKeys(base))
2550
+ defined.add(key);
2551
+ }
2552
+ let runtimeUsedKeys;
2553
+ if (config.runtimeUsage) {
2554
+ try {
2555
+ const parsed = JSON.parse(await readFile6(resolve6(config.runtimeUsage), "utf-8"));
2556
+ if (Array.isArray(parsed))
2557
+ runtimeUsedKeys = parsed.filter((k) => typeof k === "string");
2558
+ } catch (error) {
2559
+ printError(`ignoring unreadable runtime-usage file: ${error.message}`);
2560
+ }
2561
+ }
2562
+ const { scan, errors: scanErrors } = await audit.scanSources(sources, {
2563
+ functionNames: config.functionNames
2564
+ });
2565
+ const report = audit.findUnusedKeys(defined, scan, {
2566
+ dynamicKeys: config.dynamicKeys,
2567
+ ignoreKeys: config.ignoreKeys,
2568
+ runtimeUsedKeys
2569
+ });
2570
+ const lines = [];
2571
+ for (const error of scanErrors)
2572
+ lines.push(` ! could not parse ${error.file}: ${error.message}`);
2573
+ for (const finding of report.usedButUndefined) {
2574
+ lines.push(` ✗ used but undefined: ${finding.key} (${finding.sites[0]?.file}:${finding.sites[0]?.line})`);
2575
+ }
2576
+ for (const finding of report.unused) {
2577
+ lines.push(` ! unused (${finding.tier}): ${finding.key}`);
2578
+ }
2579
+ return {
2580
+ lines,
2581
+ errors: report.usedButUndefined.length,
2582
+ warnings: report.unused.length,
2583
+ json: {
2584
+ unused: report.unused,
2585
+ usedButUndefined: report.usedButUndefined,
2586
+ dynamicPrefixCoverage: report.dynamicPrefixCoverage,
2587
+ opaqueSiteCount: report.opaqueSiteCount,
2588
+ scanErrors
2589
+ }
2590
+ };
2591
+ }
2592
+ async function runHardcoded(audit, config, sources) {
2593
+ const lines = [];
2594
+ const all = [];
2595
+ let warnings = 0;
2596
+ for (const source of sources) {
2597
+ if (!source.file.endsWith(".svelte"))
2598
+ continue;
2599
+ try {
2600
+ const findings = await audit.findHardcodedStrings(source.code, source.file, {
2601
+ ignoreStrings: config.ignoreStrings
2602
+ });
2603
+ warnings += findings.length;
2604
+ all.push(...findings);
2605
+ for (const f of findings) {
2606
+ const where = f.kind === "attribute" ? `@${f.attribute}` : "text";
2607
+ lines.push(` ! hardcoded ${where}: "${f.text}" (${f.file}:${f.line})`);
2608
+ }
2609
+ } catch (error) {
2610
+ lines.push(` ! could not parse ${source.file}: ${error.message}`);
2611
+ }
2612
+ }
2613
+ return { lines, errors: 0, warnings, json: { findings: all } };
2614
+ }
2615
+ async function runI18n(positionals, flags) {
2616
+ const first = positionals[0];
2617
+ const check = first && isCheck(first) ? first : "audit";
2618
+ const dirArgs = (first && isCheck(first) ? positionals.slice(1) : positionals).filter(Boolean);
2619
+ const asJson = boolFlag(flags, "json");
2620
+ const strict = boolFlag(flags, "strict");
2621
+ let audit;
2622
+ try {
2623
+ audit = await import("@urbicon-ui/i18n/audit");
2624
+ } catch (error) {
2625
+ printError(`cannot load @urbicon-ui/i18n/audit — is @urbicon-ui/i18n installed? (${error.message})`);
2626
+ return EXIT.FAIL;
2627
+ }
2628
+ let config;
2629
+ try {
2630
+ config = await loadConfig(flags, dirArgs);
2631
+ } catch (error) {
2632
+ printError(error.message);
2633
+ return EXIT.USAGE;
2634
+ }
2635
+ const wantsParity = check === "parity" || check === "audit";
2636
+ const wantsUnused = check === "unused" || check === "audit";
2637
+ const wantsHardcoded = check === "hardcoded" || check === "audit";
2638
+ let sources = [];
2639
+ if (wantsUnused || wantsHardcoded) {
2640
+ const files = (await Promise.all(config.sources.map((dir) => collectFiles(resolve6(dir), [".ts", ".js", ".svelte"])))).flat();
2641
+ sources = await Promise.all(files.map(async (file) => ({ file: label(file), code: await readFile6(file, "utf-8") })));
2642
+ }
2643
+ let groups = [];
2644
+ let bundleErrors = [];
2645
+ if (wantsParity || wantsUnused) {
2646
+ const loaded = await loadBundleGroups(config.translations, audit.isLocaleSupported);
2647
+ groups = loaded.groups;
2648
+ bundleErrors = loaded.errors;
2649
+ }
2650
+ const sections = {};
2651
+ if (wantsParity)
2652
+ sections.parity = runParity(audit, config, groups);
2653
+ if (wantsUnused)
2654
+ sections.unused = await runUnused(audit, config, groups, sources);
2655
+ if (wantsHardcoded)
2656
+ sections.hardcoded = await runHardcoded(audit, config, sources);
2657
+ const totalErrors = Object.values(sections).reduce((sum, s) => sum + s.errors, 0);
2658
+ const totalWarnings = Object.values(sections).reduce((sum, s) => sum + s.warnings, 0);
2659
+ const bundleFailed = (wantsParity || wantsUnused) && bundleErrors.length > 0;
2660
+ const failed = totalErrors > 0 || bundleFailed || strict && totalWarnings > 0;
2661
+ if (asJson) {
2662
+ console.log(JSON.stringify({
2663
+ ok: !failed,
2664
+ check,
2665
+ strict,
2666
+ bundleErrors,
2667
+ ...Object.fromEntries(Object.entries(sections).map(([k, v]) => [k, v.json]))
2668
+ }, null, 2));
2669
+ return failed ? EXIT.FAIL : EXIT.OK;
2670
+ }
2671
+ for (const error of bundleErrors)
2672
+ printError(error);
2673
+ for (const [name, section] of Object.entries(sections)) {
2674
+ console.log(`
2675
+ ${name}:`);
2676
+ if (section.lines.length === 0)
2677
+ console.log(" ✓ no findings");
2678
+ else
2679
+ for (const line of section.lines)
2680
+ console.log(line);
2681
+ }
2682
+ const bundleNote = bundleErrors.length ? `, ${bundleErrors.length} bundle error(s)` : "";
2683
+ console.log(`
2684
+ ${totalErrors} error(s), ${totalWarnings} advisory finding(s)${bundleNote}${strict ? " (--strict: advisory gates)" : ""}.`);
2685
+ if (failed)
2686
+ console.log("FAIL.");
2687
+ return failed ? EXIT.FAIL : EXIT.OK;
2688
+ }
2689
+
2404
2690
  // src/cli/commands/init.ts
2405
- import { mkdir, readFile as readFile7, writeFile as writeFile2 } from "node:fs/promises";
2406
- import { dirname as dirname5, join as join3, relative as relative2, resolve as resolve7 } from "node:path";
2691
+ import { mkdir, readFile as readFile8, writeFile as writeFile2 } from "node:fs/promises";
2692
+ import { dirname as dirname5, join as join4, relative as relative3, resolve as resolve8 } from "node:path";
2407
2693
 
2408
2694
  // src/cli/package-root.ts
2409
- import { readFile as readFile6 } from "node:fs/promises";
2410
- import { dirname as dirname4, resolve as resolve6 } from "node:path";
2695
+ import { readFile as readFile7 } from "node:fs/promises";
2696
+ import { dirname as dirname4, resolve as resolve7 } from "node:path";
2411
2697
  import { fileURLToPath as fileURLToPath2 } from "node:url";
2412
2698
  async function findPackageRoot() {
2413
2699
  let dir = dirname4(fileURLToPath2(import.meta.url));
2414
2700
  for (let i = 0;i < 6; i++) {
2415
2701
  try {
2416
- const pkg = JSON.parse(await readFile6(resolve6(dir, "package.json"), "utf-8"));
2702
+ const pkg = JSON.parse(await readFile7(resolve7(dir, "package.json"), "utf-8"));
2417
2703
  if (pkg.name === "@urbicon-ui/design")
2418
2704
  return dir;
2419
2705
  } catch {}
@@ -2452,11 +2738,11 @@ async function readTemplate(name) {
2452
2738
  const root = await findPackageRoot();
2453
2739
  if (!root)
2454
2740
  throw new Error("could not locate the @urbicon-ui/design package root");
2455
- return readFile7(join3(root, "templates", name), "utf-8");
2741
+ return readFile8(join4(root, "templates", name), "utf-8");
2456
2742
  }
2457
2743
  async function readOrNull(path) {
2458
2744
  try {
2459
- return await readFile7(path, "utf-8");
2745
+ return await readFile8(path, "utf-8");
2460
2746
  } catch {
2461
2747
  return null;
2462
2748
  }
@@ -2471,14 +2757,14 @@ function upsertBlock(existing, block) {
2471
2757
  const content = existing.slice(0, startIdx) + block.trim() + existing.slice(endIdx + BLOCK_END.length);
2472
2758
  return { content, replaced: true };
2473
2759
  }
2474
- const sep2 = existing.length === 0 ? "" : existing.endsWith(`
2760
+ const sep3 = existing.length === 0 ? "" : existing.endsWith(`
2475
2761
 
2476
2762
  `) ? "" : existing.endsWith(`
2477
2763
  `) ? `
2478
2764
  ` : `
2479
2765
 
2480
2766
  `;
2481
- return { content: `${existing}${sep2}${block.trim()}
2767
+ return { content: `${existing}${sep3}${block.trim()}
2482
2768
  `, replaced: false };
2483
2769
  }
2484
2770
  async function mergeHook(settingsPath) {
@@ -2507,7 +2793,7 @@ async function mergeHook(settingsPath) {
2507
2793
  }
2508
2794
  async function runInit(_positionals, flags) {
2509
2795
  const cwd = process.cwd();
2510
- const rel = (p) => relative2(cwd, p) || p;
2796
+ const rel = (p) => relative3(cwd, p) || p;
2511
2797
  const done = [];
2512
2798
  const skipped = [];
2513
2799
  let block;
@@ -2517,7 +2803,7 @@ async function runInit(_positionals, flags) {
2517
2803
  printError(err.message);
2518
2804
  return EXIT.FAIL;
2519
2805
  }
2520
- const agentsPath = resolve7(stringFlag(flags, "agents-file") ?? "AGENTS.md");
2806
+ const agentsPath = resolve8(stringFlag(flags, "agents-file") ?? "AGENTS.md");
2521
2807
  const existingAgents = await readOrNull(agentsPath) ?? "";
2522
2808
  let upserted;
2523
2809
  try {
@@ -2537,7 +2823,7 @@ async function runInit(_positionals, flags) {
2537
2823
  done.push(`${rel(manifestPath)} — scaffolded`);
2538
2824
  }
2539
2825
  if (boolFlag(flags, "hook")) {
2540
- const settingsPath = resolve7(".claude", "settings.json");
2826
+ const settingsPath = resolve8(".claude", "settings.json");
2541
2827
  try {
2542
2828
  const result = await mergeHook(settingsPath);
2543
2829
  (result === "added" ? done : skipped).push(`${rel(settingsPath)} — ${result === "added" ? "wired" : "already has"} the PostToolUse \`urbicon hook\``);
@@ -2546,7 +2832,7 @@ async function runInit(_positionals, flags) {
2546
2832
  }
2547
2833
  }
2548
2834
  if (boolFlag(flags, "ci")) {
2549
- const ciPath = resolve7(".github", "workflows", "design-gate.yml");
2835
+ const ciPath = resolve8(".github", "workflows", "design-gate.yml");
2550
2836
  if (await readOrNull(ciPath)) {
2551
2837
  skipped.push(`${rel(ciPath)} — already present`);
2552
2838
  } else {
@@ -2658,9 +2944,9 @@ async function runSyncManifest(_positionals, flags) {
2658
2944
  }
2659
2945
 
2660
2946
  // src/cli/commands/validate.ts
2661
- import { readdir as readdir2, readFile as readFile8, stat as stat2 } from "node:fs/promises";
2662
- import { join as join4, relative as relative3, resolve as resolve8, sep as sep2 } from "node:path";
2663
- var SKIP_DIRS2 = new Set([
2947
+ import { readdir as readdir3, readFile as readFile9, stat as stat3 } from "node:fs/promises";
2948
+ import { join as join5, relative as relative4, resolve as resolve9, sep as sep3 } from "node:path";
2949
+ var SKIP_DIRS3 = new Set([
2664
2950
  "node_modules",
2665
2951
  ".svelte-kit",
2666
2952
  ".git",
@@ -2670,27 +2956,27 @@ var SKIP_DIRS2 = new Set([
2670
2956
  ".turbo",
2671
2957
  "coverage"
2672
2958
  ]);
2673
- var MAX_DEPTH2 = 24;
2674
- function label(abs) {
2675
- return relative3(process.cwd(), abs).split(sep2).join("/") || abs;
2959
+ var MAX_DEPTH3 = 24;
2960
+ function label2(abs) {
2961
+ return relative4(process.cwd(), abs).split(sep3).join("/") || abs;
2676
2962
  }
2677
2963
  async function collectSvelte(dir, depth = 0) {
2678
- if (depth > MAX_DEPTH2)
2964
+ if (depth > MAX_DEPTH3)
2679
2965
  return [];
2680
2966
  let entries;
2681
2967
  try {
2682
- entries = await readdir2(dir, { withFileTypes: true });
2968
+ entries = await readdir3(dir, { withFileTypes: true });
2683
2969
  } catch {
2684
2970
  return [];
2685
2971
  }
2686
2972
  const files = [];
2687
2973
  for (const entry of entries) {
2688
2974
  if (entry.isDirectory()) {
2689
- if (SKIP_DIRS2.has(entry.name) || entry.name.startsWith("."))
2975
+ if (SKIP_DIRS3.has(entry.name) || entry.name.startsWith("."))
2690
2976
  continue;
2691
- files.push(...await collectSvelte(join4(dir, entry.name), depth + 1));
2977
+ files.push(...await collectSvelte(join5(dir, entry.name), depth + 1));
2692
2978
  } else if (entry.isFile() && entry.name.endsWith(".svelte")) {
2693
- files.push(join4(dir, entry.name));
2979
+ files.push(join5(dir, entry.name));
2694
2980
  }
2695
2981
  }
2696
2982
  return files;
@@ -2711,20 +2997,20 @@ async function gather(positionals) {
2711
2997
  units.push({ label: "<stdin>", code: await readStdin2() });
2712
2998
  continue;
2713
2999
  }
2714
- const abs = resolve8(p);
3000
+ const abs = resolve9(p);
2715
3001
  let info;
2716
3002
  try {
2717
- info = await stat2(abs);
3003
+ info = await stat3(abs);
2718
3004
  } catch {
2719
3005
  printError(`cannot read "${p}"`);
2720
3006
  return null;
2721
3007
  }
2722
3008
  if (info.isDirectory()) {
2723
3009
  for (const file of await collectSvelte(abs)) {
2724
- units.push({ label: label(file), code: await readFile8(file, "utf-8") });
3010
+ units.push({ label: label2(file), code: await readFile9(file, "utf-8") });
2725
3011
  }
2726
3012
  } else {
2727
- units.push({ label: label(abs), code: await readFile8(abs, "utf-8") });
3013
+ units.push({ label: label2(abs), code: await readFile9(abs, "utf-8") });
2728
3014
  }
2729
3015
  }
2730
3016
  return units;
@@ -2785,7 +3071,7 @@ ${reports.length} file(s) · ${totals.error} error(s), ` + `${totals.warning} wa
2785
3071
  }
2786
3072
  if (extraTokens.length > 0) {
2787
3073
  console.log(`
2788
- ${extraTokens.length} project token override(s) applied from ${relative3(process.cwd(), manifestPath).split(sep2).join("/")}.`);
3074
+ ${extraTokens.length} project token override(s) applied from ${relative4(process.cwd(), manifestPath).split(sep3).join("/")}.`);
2789
3075
  }
2790
3076
  if (gate.slopBreaches.length > 0) {
2791
3077
  console.log(`
@@ -2802,12 +3088,12 @@ FAIL — ${reason}.`);
2802
3088
  }
2803
3089
 
2804
3090
  // src/cli/commands/verb.ts
2805
- import { readdir as readdir3, readFile as readFile9 } from "node:fs/promises";
2806
- import { resolve as resolve9 } from "node:path";
3091
+ import { readdir as readdir4, readFile as readFile10 } from "node:fs/promises";
3092
+ import { resolve as resolve10 } from "node:path";
2807
3093
  var SAFE_VERB = /^[a-z][a-z0-9-]*$/;
2808
3094
  async function resolveVerbsDir() {
2809
3095
  const root = await findPackageRoot();
2810
- return root ? resolve9(root, "skill", "verbs") : null;
3096
+ return root ? resolve10(root, "skill", "verbs") : null;
2811
3097
  }
2812
3098
  function purposeOf(body) {
2813
3099
  const heading = body.split(`
@@ -2823,7 +3109,7 @@ async function runVerbList(_positionals, _flags) {
2823
3109
  }
2824
3110
  let files;
2825
3111
  try {
2826
- files = (await readdir3(dir)).filter((f) => f.endsWith(".md")).sort();
3112
+ files = (await readdir4(dir)).filter((f) => f.endsWith(".md")).sort();
2827
3113
  } catch {
2828
3114
  printError(`no verb recipes found at ${dir}`);
2829
3115
  return EXIT.FAIL;
@@ -2833,7 +3119,7 @@ async function runVerbList(_positionals, _flags) {
2833
3119
  const name = file.replace(/\.md$/, "");
2834
3120
  let purpose = "";
2835
3121
  try {
2836
- purpose = purposeOf(await readFile9(resolve9(dir, file), "utf-8"));
3122
+ purpose = purposeOf(await readFile10(resolve10(dir, file), "utf-8"));
2837
3123
  } catch {}
2838
3124
  console.log(` ${name.padEnd(10)} ${purpose}`);
2839
3125
  }
@@ -2855,7 +3141,7 @@ async function runVerb(positionals, _flags) {
2855
3141
  return EXIT.FAIL;
2856
3142
  }
2857
3143
  try {
2858
- console.log(await readFile9(resolve9(dir, `${name}.md`), "utf-8"));
3144
+ console.log(await readFile10(resolve10(dir, `${name}.md`), "utf-8"));
2859
3145
  return EXIT.OK;
2860
3146
  } catch {
2861
3147
  printError(`unknown verb "${name}" — list the available verbs with \`urbicon verbs\``);
@@ -2917,6 +3203,18 @@ Commands:
2917
3203
  --src <dir> Source tree to scan (default ./src).
2918
3204
  --manifest <path> Manifest file (default ./design.manifest.md).
2919
3205
  --json Emit the scan result as JSON.
3206
+ i18n [check] [dirs…] Audit @urbicon-ui/i18n usage. check = parity | unused |
3207
+ hardcoded | audit (all, default). Run under Bun.
3208
+ --translations <d> Locale-bundle dir(s), comma-separated
3209
+ (default src/lib/translations).
3210
+ --config <path> i18n.audit.json (default ./i18n.audit.json).
3211
+ --dynamic-keys <g> Key globs built dynamically (errors.*).
3212
+ --ignore-keys <g> Key globs to skip entirely.
3213
+ --ignore-strings Hardcoded-string globs to skip.
3214
+ --base-locale <l> Parity base (default en).
3215
+ --json / --strict Machine-readable / gate advisory too.
3216
+ Gates on parity errors + used-but-undefined; unused,
3217
+ hardcoded and parity warnings are advisory.
2920
3218
  verbs List the design verbs (recipes over the design loop).
2921
3219
  verb <name> Print one verb recipe, e.g. "urbicon verb compose".
2922
3220
  help Show this help.
@@ -2938,7 +3236,7 @@ async function readVersion() {
2938
3236
  if (!root)
2939
3237
  return "unknown";
2940
3238
  try {
2941
- const pkg = JSON.parse(await readFile10(resolve10(root, "package.json"), "utf-8"));
3239
+ const pkg = JSON.parse(await readFile11(resolve11(root, "package.json"), "utf-8"));
2942
3240
  return pkg.version ?? "unknown";
2943
3241
  } catch {
2944
3242
  return "unknown";
@@ -2971,6 +3269,8 @@ async function main(argv) {
2971
3269
  return runRecordDecision(positionals, flags);
2972
3270
  case "sync-manifest":
2973
3271
  return runSyncManifest(positionals, flags);
3272
+ case "i18n":
3273
+ return runI18n(positionals, flags);
2974
3274
  case "verbs":
2975
3275
  return runVerbList(positionals, flags);
2976
3276
  case "verb":
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@urbicon-ui/design",
3
- "version": "6.3.7",
3
+ "version": "6.3.9",
4
4
  "description": "The urbicon CLI — version-pinned design validation and design-manifest tooling for projects built with Urbicon UI. Wraps @urbicon-ui/design-engine for editor hooks and CI.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -30,7 +30,7 @@
30
30
  ],
31
31
  "sideEffects": false,
32
32
  "scripts": {
33
- "build": "bun build ./src/cli/index.ts --target node --outfile dist/cli.js",
33
+ "build": "bun build ./src/cli/index.ts --target node --outfile dist/cli.js --external @urbicon-ui/i18n",
34
34
  "dev": "bun run --watch src/cli/index.ts",
35
35
  "start": "bun run src/cli/index.ts",
36
36
  "check": "tsc --noEmit",
@@ -38,8 +38,9 @@
38
38
  "test:run": "vitest run"
39
39
  },
40
40
  "dependencies": {
41
- "@urbicon-ui/design-content": "6.3.7",
42
- "@urbicon-ui/design-engine": "6.3.7"
41
+ "@urbicon-ui/design-content": "6.3.9",
42
+ "@urbicon-ui/design-engine": "6.3.9",
43
+ "@urbicon-ui/i18n": "6.3.9"
43
44
  },
44
45
  "devDependencies": {
45
46
  "typescript": "^6.0.3",
@@ -24,3 +24,9 @@ jobs:
24
24
  # fails below the floor); add --record to append a drift point to the
25
25
  # design.manifest.history.ndjson sidecar.
26
26
  - run: bunx urbicon validate src/ --json
27
+ # i18n audit — gates on translation-parity errors + keys used in code but
28
+ # missing from the bundles. Unused keys and hardcoded UI strings are
29
+ # advisory (add --strict to gate them too). Loads .ts locale bundles, so it
30
+ # needs Bun; point --translations at your bundle dir(s). Drop this step if
31
+ # the project does not use @urbicon-ui/i18n.
32
+ - run: bunx urbicon i18n audit src/ --translations src/lib/translations