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.cjs +156 -6
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.js +156 -6
- package/dist/cli/index.js.map +1 -1
- package/dist/index.cjs +22 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +22 -4
- package/dist/index.js.map +1 -1
- package/dist/runtime.cjs.map +1 -1
- package/dist/runtime.js.map +1 -1
- package/dist/src/storage/translations.d.ts +8 -0
- package/dist/src/storage/translations.d.ts.map +1 -1
- package/dist/src/translate/prompts/translation-prompt.d.ts +2 -0
- package/dist/src/translate/prompts/translation-prompt.d.ts.map +1 -1
- package/dist/src/translate/worklist.d.ts +4 -1
- package/dist/src/translate/worklist.d.ts.map +1 -1
- package/dist/studio/server.cjs +141 -3
- package/dist/studio/server.cjs.map +1 -1
- package/dist/studio/server.d.ts.map +1 -1
- package/dist/studio/server.js +141 -3
- package/dist/studio/server.js.map +1 -1
- package/package.json +1 -1
package/dist/cli/index.cjs
CHANGED
|
@@ -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 (
|
|
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
|
-
|
|
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 =
|
|
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) {
|