scribe-cms 0.0.16 → 0.0.17

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/index.js CHANGED
@@ -756,6 +756,15 @@ function listTranslationsForLocale(db, contentType, locale) {
756
756
  function bulkLoadTranslations(db) {
757
757
  return db.prepare(`SELECT * FROM translations ORDER BY content_type, en_slug, locale`).all();
758
758
  }
759
+ function countTranslations(db) {
760
+ const row = db.prepare(`SELECT COUNT(*) as count FROM translations`).get();
761
+ return row.count;
762
+ }
763
+ function latestTranslationAtByLocale(db) {
764
+ return db.prepare(
765
+ `SELECT locale, MAX(translated_at) as latest FROM translations GROUP BY locale`
766
+ ).all();
767
+ }
759
768
 
760
769
  // src/translate/validate-mdx-body.ts
761
770
  init_esm_shims();
@@ -2114,6 +2123,11 @@ function computePageEnHash(translatableFrontmatter, body) {
2114
2123
 
2115
2124
  // src/translate/worklist.ts
2116
2125
  init_sqlite();
2126
+ function parseContentTypeFilter(contentType) {
2127
+ if (!contentType) return void 0;
2128
+ const ids = contentType.split(",").map((id) => id.trim()).filter(Boolean);
2129
+ return ids.length > 0 ? new Set(ids) : void 0;
2130
+ }
2117
2131
  function listEnSlugs4(rootDir, contentDir) {
2118
2132
  const dir = path4.join(rootDir, contentDir);
2119
2133
  if (!fs2.existsSync(dir)) return [];
@@ -2123,8 +2137,9 @@ function buildWorklist(config, options = {}) {
2123
2137
  const db = openStore(config, "readonly");
2124
2138
  const items = [];
2125
2139
  const locales = options.locales ?? config.locales.filter((locale) => locale !== config.defaultLocale);
2140
+ const contentTypes = parseContentTypeFilter(options.contentType);
2126
2141
  for (const type of config.types) {
2127
- if (options.contentType && type.id !== options.contentType) continue;
2142
+ if (contentTypes && !contentTypes.has(type.id)) continue;
2128
2143
  const enSlugs = options.enSlug ? [options.enSlug] : listEnSlugs4(config.rootDir, type.contentDir);
2129
2144
  for (const enSlug of enSlugs) {
2130
2145
  const enDoc = readEnDocument(config, type, enSlug);
@@ -2144,12 +2159,13 @@ function buildWorklist(config, options = {}) {
2144
2159
  });
2145
2160
  continue;
2146
2161
  }
2147
- if (existing.en_hash !== currentEnHash) {
2162
+ const stale = existing.en_hash !== currentEnHash;
2163
+ if (stale || options.force) {
2148
2164
  items.push({
2149
2165
  contentType: type.id,
2150
2166
  enSlug,
2151
2167
  locale,
2152
- reason: "stale",
2168
+ reason: stale ? "stale" : "forced",
2153
2169
  currentEnHash,
2154
2170
  storedEnHash: existing.en_hash
2155
2171
  });
@@ -3928,12 +3944,21 @@ function renderLayout(title, body, project, options = {}) {
3928
3944
  .tag { font-size: var(--fs-sm); color: var(--dim); margin-left: 6px; }
3929
3945
  .tag-warn { color: var(--stale); }
3930
3946
  .tag-err { color: var(--missing); }
3947
+
3948
+ /* dashboard */
3949
+ .cards { display: flex; flex-wrap: wrap; gap: 1px; background: var(--border); border-bottom: 1px solid var(--border); }
3950
+ .card { flex: 1 1 120px; background: var(--bg); padding: 12px; min-width: 120px; }
3951
+ .card-value { font-size: 22px; font-weight: 300; line-height: 1.2; }
3952
+ .card-label { font-size: var(--fs-sm); color: var(--dim); margin-top: 2px; }
3953
+ .bar { display: inline-block; width: 120px; height: 6px; background: var(--border); border-radius: 3px; overflow: hidden; vertical-align: middle; margin-right: 8px; }
3954
+ .bar-fill { display: block; height: 100%; }
3931
3955
  </style>
3932
3956
  </head>
3933
3957
  <body>
3934
3958
  <div class="app">
3935
3959
  <nav class="actbar">
3936
3960
  <a href="/" title="Overview">\u2302</a>
3961
+ <a href="/dashboard" title="Dashboard">\u25A6</a>
3937
3962
  <a href="/staleness" title="Staleness">\u26A0</a>
3938
3963
  </nav>
3939
3964
  <aside class="sidebar">
@@ -3990,6 +4015,119 @@ async function startStudio(project, options = {}) {
3990
4015
  </table>`;
3991
4016
  return c.html(renderLayout("Overview", html, project));
3992
4017
  });
4018
+ app.get("/dashboard", (c) => {
4019
+ const db = openStore(config, "readonly");
4020
+ const types = project.listTypes();
4021
+ const targetLocales = config.locales.filter((l) => l !== config.defaultLocale);
4022
+ const docCountByType = /* @__PURE__ */ new Map();
4023
+ let totalEnDocs = 0;
4024
+ for (const type of types) {
4025
+ const n = type.list().length;
4026
+ docCountByType.set(type.id, n);
4027
+ totalEnDocs += n;
4028
+ }
4029
+ const worklist = buildWorklist(config);
4030
+ const missingByLocale = /* @__PURE__ */ new Map();
4031
+ const staleByLocale = /* @__PURE__ */ new Map();
4032
+ const missingByType = /* @__PURE__ */ new Map();
4033
+ const staleByType = /* @__PURE__ */ new Map();
4034
+ let missingTotal = 0;
4035
+ let staleTotal = 0;
4036
+ for (const item of worklist) {
4037
+ if (item.reason === "missing") {
4038
+ missingByLocale.set(item.locale, (missingByLocale.get(item.locale) ?? 0) + 1);
4039
+ missingByType.set(item.contentType, (missingByType.get(item.contentType) ?? 0) + 1);
4040
+ missingTotal++;
4041
+ } else if (item.reason === "stale") {
4042
+ staleByLocale.set(item.locale, (staleByLocale.get(item.locale) ?? 0) + 1);
4043
+ staleByType.set(item.contentType, (staleByType.get(item.contentType) ?? 0) + 1);
4044
+ staleTotal++;
4045
+ }
4046
+ }
4047
+ const storedTranslations = countTranslations(db);
4048
+ const latestByLocale = /* @__PURE__ */ new Map();
4049
+ for (const row of latestTranslationAtByLocale(db)) {
4050
+ if (row.latest) latestByLocale.set(row.locale, row.latest);
4051
+ }
4052
+ db.close();
4053
+ const expectedTotal = totalEnDocs * targetLocales.length;
4054
+ const coverageTotal = expectedTotal > 0 ? (expectedTotal - missingTotal) / expectedTotal : 0;
4055
+ const coveragePct = expectedTotal > 0 ? Math.round(coverageTotal * 100) : 0;
4056
+ const pct = (fraction) => Math.round(fraction * 100);
4057
+ const num = (n, warnClass) => n > 0 ? `<span class="${warnClass}">${n}</span>` : `<span class="dim">0</span>`;
4058
+ const coverageColor = expectedTotal > 0 && coveragePct === 100 ? ` style="color:var(--ok)"` : "";
4059
+ const coverageValue = expectedTotal > 0 ? `${coveragePct}%` : `<span class="dim">\u2014</span>`;
4060
+ const staleColor = staleTotal > 0 ? ` style="color:var(--stale)"` : "";
4061
+ const missingColor = missingTotal > 0 ? ` style="color:var(--missing)"` : "";
4062
+ const cards = `<div class="cards">
4063
+ <div class="card"><div class="card-value">${totalEnDocs}</div><div class="card-label">Documents (EN)</div></div>
4064
+ <div class="card"><div class="card-value">${targetLocales.length}</div><div class="card-label">Target locales</div></div>
4065
+ <div class="card"><div class="card-value">${storedTranslations}</div><div class="card-label">Stored translations</div></div>
4066
+ <div class="card"><div class="card-value"${coverageColor}>${coverageValue}</div><div class="card-label">Coverage</div></div>
4067
+ <div class="card"><div class="card-value"${staleColor}>${staleTotal}</div><div class="card-label">Stale</div></div>
4068
+ <div class="card"><div class="card-value"${missingColor}>${missingTotal}</div><div class="card-label">Missing</div></div>
4069
+ </div>`;
4070
+ let localeRows;
4071
+ if (totalEnDocs === 0 || targetLocales.length === 0) {
4072
+ localeRows = `<tr><td colspan="7" class="dim">No target locales or documents</td></tr>`;
4073
+ } else {
4074
+ localeRows = targetLocales.map((locale) => {
4075
+ const expected = totalEnDocs;
4076
+ const missing = missingByLocale.get(locale) ?? 0;
4077
+ const stale = staleByLocale.get(locale) ?? 0;
4078
+ const translated = expected - missing;
4079
+ const upToDate = translated - stale;
4080
+ const coverage = expected > 0 ? translated / expected : 0;
4081
+ const covPct = pct(coverage);
4082
+ const fillColor = covPct === 100 ? "var(--ok)" : "var(--stale)";
4083
+ const fallbacks = config.localeFallbacks[locale]?.length ? escapeHtml(config.localeFallbacks[locale].join(" \u2192 ")) : `<span class="dim">\u2014</span>`;
4084
+ const latest = latestByLocale.get(locale);
4085
+ const last = latest ? escapeHtml(latest.slice(0, 10)) : `<span class="dim">\u2014</span>`;
4086
+ return `<tr>
4087
+ <td>${escapeHtml(locale)}</td>
4088
+ <td><span class="bar"><span class="bar-fill" style="width:${covPct}%;background:${fillColor}"></span></span>${covPct}%</td>
4089
+ <td>${upToDate}</td>
4090
+ <td>${num(stale, "tag-warn")}</td>
4091
+ <td>${num(missing, "tag-err")}</td>
4092
+ <td>${fallbacks}</td>
4093
+ <td>${last}</td>
4094
+ </tr>`;
4095
+ }).join("");
4096
+ }
4097
+ let typeRows;
4098
+ if (types.length === 0 || targetLocales.length === 0) {
4099
+ typeRows = `<tr><td colspan="5" class="dim">No types or target locales</td></tr>`;
4100
+ } else {
4101
+ typeRows = types.map((type) => {
4102
+ const stale = staleByType.get(type.id) ?? 0;
4103
+ const missing = missingByType.get(type.id) ?? 0;
4104
+ return `<tr>
4105
+ <td><a href="/type/${encodePathSegment(type.id)}">${escapeHtml(type.config.label)}</a></td>
4106
+ <td class="dim">${escapeHtml(type.id)}</td>
4107
+ <td>${docCountByType.get(type.id) ?? 0}</td>
4108
+ <td>${num(stale, "tag-warn")}</td>
4109
+ <td>${num(missing, "tag-err")}</td>
4110
+ </tr>`;
4111
+ }).join("");
4112
+ }
4113
+ const html = `<div class="toolbar">Dashboard</div>
4114
+ ${cards}
4115
+ <div class="section">
4116
+ <div class="section-head">Locales</div>
4117
+ <table class="data">
4118
+ <thead><tr><th>Locale</th><th>Coverage</th><th>Up to date</th><th>Stale</th><th>Missing</th><th>Fallbacks</th><th>Last translated</th></tr></thead>
4119
+ <tbody>${localeRows}</tbody>
4120
+ </table>
4121
+ </div>
4122
+ <div class="section">
4123
+ <div class="section-head">Types</div>
4124
+ <table class="data">
4125
+ <thead><tr><th>Type</th><th>ID</th><th>EN docs</th><th>Stale</th><th>Missing</th></tr></thead>
4126
+ <tbody>${typeRows}</tbody>
4127
+ </table>
4128
+ </div>`;
4129
+ return c.html(renderLayout("Dashboard", html, project));
4130
+ });
3993
4131
  app.get("/type/:id", (c) => {
3994
4132
  const typeId = c.req.param("id");
3995
4133
  const type = project.getType(typeId);
@@ -4757,7 +4895,7 @@ Export-static flags:
4757
4895
  --locale <code>... Locales to export (default: all configured)
4758
4896
 
4759
4897
  Translate flags:
4760
- --type <id> Content type (interactive picker in a TTY when omitted)
4898
+ --type <id> Content type, or comma-separated ids (interactive picker in a TTY when omitted)
4761
4899
  --preset <name> Locale preset from config (interactive picker in a TTY)
4762
4900
  --locale <code>... Target locale(s); overrides --preset
4763
4901
  --slug <en-slug> Single English document
@@ -4849,7 +4987,8 @@ mode to use.
4849
4987
  contentType: selection.contentType,
4850
4988
  locales,
4851
4989
  enSlug: options.slug,
4852
- strategy: selection.strategy ?? "all"
4990
+ strategy: selection.strategy ?? "all",
4991
+ force: options.force
4853
4992
  });
4854
4993
  if (worklist.length === 0) {
4855
4994
  if (!options.dryRun) {