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.
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
  });
@@ -2530,6 +2546,13 @@ function defaultLocalizationPrompt(localeName, locale) {
2530
2546
  "Write as if a native speaker authored it for the target market."
2531
2547
  ].join(" ");
2532
2548
  }
2549
+ function buildLocalizationPrompt(promptOverride, localeName, locale) {
2550
+ const localeDirective = defaultLocalizationPrompt(localeName, locale);
2551
+ if (!promptOverride) return localeDirective;
2552
+ return `${promptOverride}
2553
+
2554
+ ${localeDirective}`;
2555
+ }
2533
2556
  var TASK_FRAMING = "You are localizing the content below into a target language specified at the end of this prompt.";
2534
2557
  function buildOutputFormatLine(hasFrontmatter, slugStrategy) {
2535
2558
  if (!hasFrontmatter) {
@@ -2546,7 +2569,11 @@ function buildRetryContextSection(previousError) {
2546
2569
  }
2547
2570
  function buildPageTranslationPrompt(input) {
2548
2571
  const localeName = LOCALE_NAMES[input.targetLocale] ?? input.targetLocale;
2549
- const localizationPrompt = input.resolved.promptOverride ?? defaultLocalizationPrompt(localeName, input.targetLocale);
2572
+ const localizationPrompt = buildLocalizationPrompt(
2573
+ input.resolved.promptOverride,
2574
+ localeName,
2575
+ input.targetLocale
2576
+ );
2550
2577
  const prefix = [
2551
2578
  TASK_FRAMING,
2552
2579
  "",
@@ -3917,12 +3944,21 @@ function renderLayout(title, body, project, options = {}) {
3917
3944
  .tag { font-size: var(--fs-sm); color: var(--dim); margin-left: 6px; }
3918
3945
  .tag-warn { color: var(--stale); }
3919
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%; }
3920
3955
  </style>
3921
3956
  </head>
3922
3957
  <body>
3923
3958
  <div class="app">
3924
3959
  <nav class="actbar">
3925
3960
  <a href="/" title="Overview">\u2302</a>
3961
+ <a href="/dashboard" title="Dashboard">\u25A6</a>
3926
3962
  <a href="/staleness" title="Staleness">\u26A0</a>
3927
3963
  </nav>
3928
3964
  <aside class="sidebar">
@@ -3979,6 +4015,119 @@ async function startStudio(project, options = {}) {
3979
4015
  </table>`;
3980
4016
  return c.html(renderLayout("Overview", html, project));
3981
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
+ });
3982
4131
  app.get("/type/:id", (c) => {
3983
4132
  const typeId = c.req.param("id");
3984
4133
  const type = project.getType(typeId);
@@ -4746,7 +4895,7 @@ Export-static flags:
4746
4895
  --locale <code>... Locales to export (default: all configured)
4747
4896
 
4748
4897
  Translate flags:
4749
- --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)
4750
4899
  --preset <name> Locale preset from config (interactive picker in a TTY)
4751
4900
  --locale <code>... Target locale(s); overrides --preset
4752
4901
  --slug <en-slug> Single English document
@@ -4838,7 +4987,8 @@ mode to use.
4838
4987
  contentType: selection.contentType,
4839
4988
  locales,
4840
4989
  enSlug: options.slug,
4841
- strategy: selection.strategy ?? "all"
4990
+ strategy: selection.strategy ?? "all",
4991
+ force: options.force
4842
4992
  });
4843
4993
  if (worklist.length === 0) {
4844
4994
  if (!options.dryRun) {