scribe-cms 0.0.15 → 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.
@@ -773,6 +773,15 @@ function listTranslationsForLocale(db, contentType, locale) {
773
773
  function bulkLoadTranslations(db) {
774
774
  return db.prepare(`SELECT * FROM translations ORDER BY content_type, en_slug, locale`).all();
775
775
  }
776
+ function countTranslations(db) {
777
+ const row = db.prepare(`SELECT COUNT(*) as count FROM translations`).get();
778
+ return row.count;
779
+ }
780
+ function latestTranslationAtByLocale(db) {
781
+ return db.prepare(
782
+ `SELECT locale, MAX(translated_at) as latest FROM translations GROUP BY locale`
783
+ ).all();
784
+ }
776
785
 
777
786
  // src/translate/validate-mdx-body.ts
778
787
  init_cjs_shims();
@@ -2131,6 +2140,11 @@ function computePageEnHash(translatableFrontmatter, body) {
2131
2140
 
2132
2141
  // src/translate/worklist.ts
2133
2142
  init_sqlite();
2143
+ function parseContentTypeFilter(contentType) {
2144
+ if (!contentType) return void 0;
2145
+ const ids = contentType.split(",").map((id) => id.trim()).filter(Boolean);
2146
+ return ids.length > 0 ? new Set(ids) : void 0;
2147
+ }
2134
2148
  function listEnSlugs4(rootDir, contentDir) {
2135
2149
  const dir = path3__default.default.join(rootDir, contentDir);
2136
2150
  if (!fs2__default.default.existsSync(dir)) return [];
@@ -2140,8 +2154,9 @@ function buildWorklist(config, options = {}) {
2140
2154
  const db = openStore(config, "readonly");
2141
2155
  const items = [];
2142
2156
  const locales = options.locales ?? config.locales.filter((locale) => locale !== config.defaultLocale);
2157
+ const contentTypes = parseContentTypeFilter(options.contentType);
2143
2158
  for (const type of config.types) {
2144
- if (options.contentType && type.id !== options.contentType) continue;
2159
+ if (contentTypes && !contentTypes.has(type.id)) continue;
2145
2160
  const enSlugs = options.enSlug ? [options.enSlug] : listEnSlugs4(config.rootDir, type.contentDir);
2146
2161
  for (const enSlug of enSlugs) {
2147
2162
  const enDoc = readEnDocument(config, type, enSlug);
@@ -2161,12 +2176,13 @@ function buildWorklist(config, options = {}) {
2161
2176
  });
2162
2177
  continue;
2163
2178
  }
2164
- if (existing.en_hash !== currentEnHash) {
2179
+ const stale = existing.en_hash !== currentEnHash;
2180
+ if (stale || options.force) {
2165
2181
  items.push({
2166
2182
  contentType: type.id,
2167
2183
  enSlug,
2168
2184
  locale,
2169
- reason: "stale",
2185
+ reason: stale ? "stale" : "forced",
2170
2186
  currentEnHash,
2171
2187
  storedEnHash: existing.en_hash
2172
2188
  });
@@ -2547,6 +2563,13 @@ function defaultLocalizationPrompt(localeName, locale) {
2547
2563
  "Write as if a native speaker authored it for the target market."
2548
2564
  ].join(" ");
2549
2565
  }
2566
+ function buildLocalizationPrompt(promptOverride, localeName, locale) {
2567
+ const localeDirective = defaultLocalizationPrompt(localeName, locale);
2568
+ if (!promptOverride) return localeDirective;
2569
+ return `${promptOverride}
2570
+
2571
+ ${localeDirective}`;
2572
+ }
2550
2573
  var TASK_FRAMING = "You are localizing the content below into a target language specified at the end of this prompt.";
2551
2574
  function buildOutputFormatLine(hasFrontmatter, slugStrategy) {
2552
2575
  if (!hasFrontmatter) {
@@ -2563,7 +2586,11 @@ function buildRetryContextSection(previousError) {
2563
2586
  }
2564
2587
  function buildPageTranslationPrompt(input) {
2565
2588
  const localeName = LOCALE_NAMES[input.targetLocale] ?? input.targetLocale;
2566
- const localizationPrompt = input.resolved.promptOverride ?? defaultLocalizationPrompt(localeName, input.targetLocale);
2589
+ const localizationPrompt = buildLocalizationPrompt(
2590
+ input.resolved.promptOverride,
2591
+ localeName,
2592
+ input.targetLocale
2593
+ );
2567
2594
  const prefix = [
2568
2595
  TASK_FRAMING,
2569
2596
  "",
@@ -3934,12 +3961,21 @@ function renderLayout(title, body, project, options = {}) {
3934
3961
  .tag { font-size: var(--fs-sm); color: var(--dim); margin-left: 6px; }
3935
3962
  .tag-warn { color: var(--stale); }
3936
3963
  .tag-err { color: var(--missing); }
3964
+
3965
+ /* dashboard */
3966
+ .cards { display: flex; flex-wrap: wrap; gap: 1px; background: var(--border); border-bottom: 1px solid var(--border); }
3967
+ .card { flex: 1 1 120px; background: var(--bg); padding: 12px; min-width: 120px; }
3968
+ .card-value { font-size: 22px; font-weight: 300; line-height: 1.2; }
3969
+ .card-label { font-size: var(--fs-sm); color: var(--dim); margin-top: 2px; }
3970
+ .bar { display: inline-block; width: 120px; height: 6px; background: var(--border); border-radius: 3px; overflow: hidden; vertical-align: middle; margin-right: 8px; }
3971
+ .bar-fill { display: block; height: 100%; }
3937
3972
  </style>
3938
3973
  </head>
3939
3974
  <body>
3940
3975
  <div class="app">
3941
3976
  <nav class="actbar">
3942
3977
  <a href="/" title="Overview">\u2302</a>
3978
+ <a href="/dashboard" title="Dashboard">\u25A6</a>
3943
3979
  <a href="/staleness" title="Staleness">\u26A0</a>
3944
3980
  </nav>
3945
3981
  <aside class="sidebar">
@@ -3996,6 +4032,119 @@ async function startStudio(project, options = {}) {
3996
4032
  </table>`;
3997
4033
  return c.html(renderLayout("Overview", html, project));
3998
4034
  });
4035
+ app.get("/dashboard", (c) => {
4036
+ const db = openStore(config, "readonly");
4037
+ const types = project.listTypes();
4038
+ const targetLocales = config.locales.filter((l) => l !== config.defaultLocale);
4039
+ const docCountByType = /* @__PURE__ */ new Map();
4040
+ let totalEnDocs = 0;
4041
+ for (const type of types) {
4042
+ const n = type.list().length;
4043
+ docCountByType.set(type.id, n);
4044
+ totalEnDocs += n;
4045
+ }
4046
+ const worklist = buildWorklist(config);
4047
+ const missingByLocale = /* @__PURE__ */ new Map();
4048
+ const staleByLocale = /* @__PURE__ */ new Map();
4049
+ const missingByType = /* @__PURE__ */ new Map();
4050
+ const staleByType = /* @__PURE__ */ new Map();
4051
+ let missingTotal = 0;
4052
+ let staleTotal = 0;
4053
+ for (const item of worklist) {
4054
+ if (item.reason === "missing") {
4055
+ missingByLocale.set(item.locale, (missingByLocale.get(item.locale) ?? 0) + 1);
4056
+ missingByType.set(item.contentType, (missingByType.get(item.contentType) ?? 0) + 1);
4057
+ missingTotal++;
4058
+ } else if (item.reason === "stale") {
4059
+ staleByLocale.set(item.locale, (staleByLocale.get(item.locale) ?? 0) + 1);
4060
+ staleByType.set(item.contentType, (staleByType.get(item.contentType) ?? 0) + 1);
4061
+ staleTotal++;
4062
+ }
4063
+ }
4064
+ const storedTranslations = countTranslations(db);
4065
+ const latestByLocale = /* @__PURE__ */ new Map();
4066
+ for (const row of latestTranslationAtByLocale(db)) {
4067
+ if (row.latest) latestByLocale.set(row.locale, row.latest);
4068
+ }
4069
+ db.close();
4070
+ const expectedTotal = totalEnDocs * targetLocales.length;
4071
+ const coverageTotal = expectedTotal > 0 ? (expectedTotal - missingTotal) / expectedTotal : 0;
4072
+ const coveragePct = expectedTotal > 0 ? Math.round(coverageTotal * 100) : 0;
4073
+ const pct = (fraction) => Math.round(fraction * 100);
4074
+ const num = (n, warnClass) => n > 0 ? `<span class="${warnClass}">${n}</span>` : `<span class="dim">0</span>`;
4075
+ const coverageColor = expectedTotal > 0 && coveragePct === 100 ? ` style="color:var(--ok)"` : "";
4076
+ const coverageValue = expectedTotal > 0 ? `${coveragePct}%` : `<span class="dim">\u2014</span>`;
4077
+ const staleColor = staleTotal > 0 ? ` style="color:var(--stale)"` : "";
4078
+ const missingColor = missingTotal > 0 ? ` style="color:var(--missing)"` : "";
4079
+ const cards = `<div class="cards">
4080
+ <div class="card"><div class="card-value">${totalEnDocs}</div><div class="card-label">Documents (EN)</div></div>
4081
+ <div class="card"><div class="card-value">${targetLocales.length}</div><div class="card-label">Target locales</div></div>
4082
+ <div class="card"><div class="card-value">${storedTranslations}</div><div class="card-label">Stored translations</div></div>
4083
+ <div class="card"><div class="card-value"${coverageColor}>${coverageValue}</div><div class="card-label">Coverage</div></div>
4084
+ <div class="card"><div class="card-value"${staleColor}>${staleTotal}</div><div class="card-label">Stale</div></div>
4085
+ <div class="card"><div class="card-value"${missingColor}>${missingTotal}</div><div class="card-label">Missing</div></div>
4086
+ </div>`;
4087
+ let localeRows;
4088
+ if (totalEnDocs === 0 || targetLocales.length === 0) {
4089
+ localeRows = `<tr><td colspan="7" class="dim">No target locales or documents</td></tr>`;
4090
+ } else {
4091
+ localeRows = targetLocales.map((locale) => {
4092
+ const expected = totalEnDocs;
4093
+ const missing = missingByLocale.get(locale) ?? 0;
4094
+ const stale = staleByLocale.get(locale) ?? 0;
4095
+ const translated = expected - missing;
4096
+ const upToDate = translated - stale;
4097
+ const coverage = expected > 0 ? translated / expected : 0;
4098
+ const covPct = pct(coverage);
4099
+ const fillColor = covPct === 100 ? "var(--ok)" : "var(--stale)";
4100
+ const fallbacks = config.localeFallbacks[locale]?.length ? escapeHtml(config.localeFallbacks[locale].join(" \u2192 ")) : `<span class="dim">\u2014</span>`;
4101
+ const latest = latestByLocale.get(locale);
4102
+ const last = latest ? escapeHtml(latest.slice(0, 10)) : `<span class="dim">\u2014</span>`;
4103
+ return `<tr>
4104
+ <td>${escapeHtml(locale)}</td>
4105
+ <td><span class="bar"><span class="bar-fill" style="width:${covPct}%;background:${fillColor}"></span></span>${covPct}%</td>
4106
+ <td>${upToDate}</td>
4107
+ <td>${num(stale, "tag-warn")}</td>
4108
+ <td>${num(missing, "tag-err")}</td>
4109
+ <td>${fallbacks}</td>
4110
+ <td>${last}</td>
4111
+ </tr>`;
4112
+ }).join("");
4113
+ }
4114
+ let typeRows;
4115
+ if (types.length === 0 || targetLocales.length === 0) {
4116
+ typeRows = `<tr><td colspan="5" class="dim">No types or target locales</td></tr>`;
4117
+ } else {
4118
+ typeRows = types.map((type) => {
4119
+ const stale = staleByType.get(type.id) ?? 0;
4120
+ const missing = missingByType.get(type.id) ?? 0;
4121
+ return `<tr>
4122
+ <td><a href="/type/${encodePathSegment(type.id)}">${escapeHtml(type.config.label)}</a></td>
4123
+ <td class="dim">${escapeHtml(type.id)}</td>
4124
+ <td>${docCountByType.get(type.id) ?? 0}</td>
4125
+ <td>${num(stale, "tag-warn")}</td>
4126
+ <td>${num(missing, "tag-err")}</td>
4127
+ </tr>`;
4128
+ }).join("");
4129
+ }
4130
+ const html = `<div class="toolbar">Dashboard</div>
4131
+ ${cards}
4132
+ <div class="section">
4133
+ <div class="section-head">Locales</div>
4134
+ <table class="data">
4135
+ <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>
4136
+ <tbody>${localeRows}</tbody>
4137
+ </table>
4138
+ </div>
4139
+ <div class="section">
4140
+ <div class="section-head">Types</div>
4141
+ <table class="data">
4142
+ <thead><tr><th>Type</th><th>ID</th><th>EN docs</th><th>Stale</th><th>Missing</th></tr></thead>
4143
+ <tbody>${typeRows}</tbody>
4144
+ </table>
4145
+ </div>`;
4146
+ return c.html(renderLayout("Dashboard", html, project));
4147
+ });
3999
4148
  app.get("/type/:id", (c) => {
4000
4149
  const typeId = c.req.param("id");
4001
4150
  const type = project.getType(typeId);
@@ -4763,7 +4912,7 @@ Export-static flags:
4763
4912
  --locale <code>... Locales to export (default: all configured)
4764
4913
 
4765
4914
  Translate flags:
4766
- --type <id> Content type (interactive picker in a TTY when omitted)
4915
+ --type <id> Content type, or comma-separated ids (interactive picker in a TTY when omitted)
4767
4916
  --preset <name> Locale preset from config (interactive picker in a TTY)
4768
4917
  --locale <code>... Target locale(s); overrides --preset
4769
4918
  --slug <en-slug> Single English document
@@ -4855,7 +5004,8 @@ mode to use.
4855
5004
  contentType: selection.contentType,
4856
5005
  locales,
4857
5006
  enSlug: options.slug,
4858
- strategy: selection.strategy ?? "all"
5007
+ strategy: selection.strategy ?? "all",
5008
+ force: options.force
4859
5009
  });
4860
5010
  if (worklist.length === 0) {
4861
5011
  if (!options.dryRun) {