@verbaly/studio 0.66.0 → 0.68.0

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,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { parseArgs, promisify } from "node:util";
3
- import { DRAFTS_FILE, check, clearDrafts, collectOrigins, counted, doctor, effectiveDrafts, extractProject, formatCliError, loadCatalogs, loadConfig, loadDrafts, markDrafts, mergeTranslations, parseCatalog, readCatalog, resolveProvider, saveDrafts, status, syncCatalogs, targetLocales, translateCatalogs, validateMessage, validatePair, writeCatalog, writeDts } from "@verbaly/compiler";
3
+ import { STATE_FILE, check, clearDrafts, collectOrigins, counted, doctor, effectiveDrafts, extractProject, formatCliError, loadCatalogs, loadConfig, loadDrafts, loadState, markDrafts, mergeTranslations, parseCatalog, readCatalog, readState, recordTranslations, resolveProvider, saveDrafts, shippedCatalogs, status, syncProject, targetLocales, translateCatalogs, validateMessage, validatePair, writeCatalog } from "@verbaly/compiler";
4
4
  import { existsSync, readFileSync, statSync } from "node:fs";
5
5
  import { join, normalize, relative, resolve } from "node:path";
6
6
  import { createServer } from "node:http";
@@ -153,23 +153,6 @@ function readAll(cfg) {
153
153
  problems
154
154
  };
155
155
  }
156
- function readDrafts(cfg) {
157
- try {
158
- return {
159
- drafts: loadDrafts(cfg),
160
- problems: []
161
- };
162
- } catch (error) {
163
- const message = error instanceof Error ? error.message : String(error);
164
- return {
165
- drafts: {},
166
- problems: [{
167
- scope: DRAFTS_FILE,
168
- message
169
- }]
170
- };
171
- }
172
- }
173
156
  function relativize(cfg, result) {
174
157
  const rel = (file) => relative(cfg.root, file).replaceAll("\\", "/");
175
158
  return {
@@ -181,14 +164,31 @@ function relativize(cfg, result) {
181
164
  extra: result.extra.map((entry) => ({
182
165
  ...entry,
183
166
  files: entry.files.map(rel)
167
+ })),
168
+ collisions: result.collisions.map((entry) => ({
169
+ ...entry,
170
+ sites: entry.sites.map((at) => ({
171
+ ...at,
172
+ file: rel(at.file)
173
+ }))
174
+ })),
175
+ divergent: result.divergent.map((entry) => ({
176
+ ...entry,
177
+ code: {
178
+ ...entry.code,
179
+ file: rel(entry.code.file)
180
+ }
184
181
  }))
185
182
  };
186
183
  }
187
184
  async function buildState(cfg) {
188
185
  const { catalogs, problems } = readAll(cfg);
189
186
  const registry = await extractProject(cfg);
190
- const sidecar = readDrafts(cfg);
191
- problems.push(...sidecar.problems);
187
+ const { state: sidecar, problem } = readState(cfg);
188
+ if (problem) problems.push({
189
+ scope: STATE_FILE,
190
+ message: problem
191
+ });
192
192
  const live = effectiveDrafts(sidecar.drafts, catalogs);
193
193
  const source = catalogs[cfg.sourceLocale] ?? {};
194
194
  const targets = targetLocales(cfg);
@@ -213,8 +213,8 @@ async function buildState(cfg) {
213
213
  scanning: cfg.include.length > 0,
214
214
  catalogs,
215
215
  origins: await collectOrigins(cfg, registry),
216
- status: status(cfg, catalogs, registry, live),
217
- check: relativize(cfg, check(cfg, catalogs, registry)),
216
+ status: status(cfg, catalogs, registry, live, sidecar.fingerprints),
217
+ check: relativize(cfg, check(cfg, catalogs, registry, sidecar.fingerprints)),
218
218
  drafts: live,
219
219
  triage: perLocale,
220
220
  problems
@@ -223,10 +223,13 @@ async function buildState(cfg) {
223
223
  function health(cfg) {
224
224
  return doctor(cfg);
225
225
  }
226
- function writeMessage(cfg, locale, key, text) {
226
+ async function writeMessage(cfg, locale, key, text) {
227
227
  if (locale === cfg.sourceLocale) throw badRequest("the source text lives in your code, Studio never edits it");
228
228
  if (!cfg.locales.includes(locale)) throw badRequest(`${locale} is not one of this project's locales`);
229
- const source = readCatalog(cfg, cfg.sourceLocale);
229
+ const before = loadState(cfg);
230
+ const catalogs = { [cfg.sourceLocale]: readCatalog(cfg, cfg.sourceLocale) };
231
+ const registry = await extractProject(cfg);
232
+ const source = shippedCatalogs(cfg, catalogs, registry, { newKeys: false })[cfg.sourceLocale];
230
233
  if (!Object.hasOwn(source, key)) throw badRequest(`"${key}" is not in the source catalog, so Studio will not create it`);
231
234
  const value = text.trim() ? text : "";
232
235
  const errors = (value ? [...validateMessage(value, locale), ...validatePair(source[key], value)] : []).filter((issue) => issue.severity === "error");
@@ -236,10 +239,15 @@ function writeMessage(cfg, locale, key, text) {
236
239
  [key]: value
237
240
  };
238
241
  writeCatalog(cfg, locale, catalog);
239
- const drafts = loadDrafts(cfg);
240
- const clearedDraft = (drafts[locale] ?? []).includes(key);
241
- clearDrafts(drafts, locale, [key]);
242
- saveDrafts(cfg, drafts);
242
+ const clearedDraft = (before.drafts[locale] ?? []).includes(key);
243
+ recordTranslations(cfg, source, [{
244
+ locale,
245
+ entries: [{
246
+ key,
247
+ text: value
248
+ }],
249
+ draft: false
250
+ }]);
243
251
  return {
244
252
  locale,
245
253
  key,
@@ -306,16 +314,13 @@ function finish(id, state, patch = {}) {
306
314
  //#region src/actions.ts
307
315
  async function runExtract(cfg) {
308
316
  if (!cfg.include.length) throw badRequest("source scanning is off (include: []), so there is nothing to extract");
309
- const registry = await extractProject(cfg);
310
- const catalogs = loadCatalogs(cfg);
311
- const result = syncCatalogs(cfg, catalogs, registry);
312
- for (const locale of cfg.locales) writeCatalog(cfg, locale, catalogs[locale] ?? {});
313
- writeDts(cfg, catalogs[cfg.sourceLocale] ?? {});
314
- const found = result.added[cfg.sourceLocale]?.length ?? 0;
317
+ const result = await syncProject(cfg);
315
318
  return {
316
319
  added: result.added,
317
- found,
318
- messages: registry.messages().size
320
+ found: result.added[cfg.sourceLocale]?.length ?? 0,
321
+ replaced: result.replaced.length,
322
+ messages: result.registry.messages().size,
323
+ ...result.stateProblem && { stateProblem: result.stateProblem }
319
324
  };
320
325
  }
321
326
  async function planTranslate(cfg, locales) {
@@ -332,30 +337,40 @@ async function planTranslate(cfg, locales) {
332
337
  };
333
338
  }
334
339
  async function startTranslate(cfg, locales) {
340
+ loadState(cfg);
335
341
  const provider = await resolveProvider(cfg);
336
342
  const job = start("translate", (await planTranslate(cfg, locales)).total);
337
343
  const id = job.id;
338
344
  (async () => {
339
345
  try {
340
- const catalogs = loadCatalogs(cfg);
346
+ const registry = await extractProject(cfg);
347
+ const catalogs = shippedCatalogs(cfg, loadCatalogs(cfg), registry, { newKeys: false });
341
348
  const result = await translateCatalogs(cfg, catalogs, provider, {
342
349
  locales,
343
350
  batchSize: cfg.translate.batchSize,
344
351
  concurrency: cfg.translate.concurrency,
345
352
  retries: cfg.translate.retries,
346
- origins: await collectOrigins(cfg),
353
+ origins: await collectOrigins(cfg, registry),
347
354
  onProgress: (p) => advance(id, p.keys, p.locale, p.error)
348
355
  });
349
- const drafts = loadDrafts(cfg);
356
+ const writes = [];
350
357
  let written = 0;
351
358
  let kept = 0;
352
359
  for (const [locale, keys] of Object.entries(result.translated)) {
353
- const landed = mergeTranslations(cfg, locale, catalogs[locale] ?? {}, keys);
354
- markDrafts(drafts, locale, landed);
360
+ const catalog = catalogs[locale] ?? {};
361
+ const landed = mergeTranslations(cfg, locale, catalog, keys);
362
+ writes.push({
363
+ locale,
364
+ draft: true,
365
+ entries: landed.map((key) => ({
366
+ key,
367
+ text: catalog[key]
368
+ }))
369
+ });
355
370
  written += landed.length;
356
371
  kept += keys.length - landed.length;
357
372
  }
358
- saveDrafts(cfg, drafts);
373
+ recordTranslations(cfg, catalogs[cfg.sourceLocale] ?? {}, writes);
359
374
  const failed = result.failed[0];
360
375
  finish(id, failed ? "failed" : "done", {
361
376
  result,
@@ -752,7 +767,7 @@ function createStudioApp(cfg, port, token) {
752
767
  const key = segment(rest.slice(1).join("/"), "key");
753
768
  const body = await readJson(req);
754
769
  if (typeof body.text !== "string") throw badRequest("body needs a \"text\" string");
755
- send(res, 200, writeMessage(cfg, locale, key, body.text));
770
+ send(res, 200, await writeMessage(cfg, locale, key, body.text));
756
771
  return;
757
772
  }
758
773
  if (req.method === "POST" && url.pathname === "/api/approve") {
package/dist/cli.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.js","names":["readJob"],"sources":["../src/http.ts","../src/triage.ts","../src/api.ts","../src/jobs.ts","../src/actions.ts","../src/history.ts","../src/assets.ts","../src/guard.ts","../src/server.ts","../src/cli.ts"],"sourcesContent":["// A client mistake is not a server fault, so the status rides with the error, not a blanket 500.\nexport class HttpError extends Error {\n readonly status: number;\n\n constructor(status: number, message: string) {\n super(message.startsWith('[verbaly]') ? message : `[verbaly] ${message}`);\n this.status = status;\n this.name = 'HttpError';\n }\n}\n\nexport const badRequest = (message: string) => new HttpError(400, message);\n\n// The absolute path of someone's project is not the browser's business.\nexport function scrub(message: string, root: string): string {\n return root ? message.split(root).join('.') : message;\n}\n","import type { Catalog } from '@verbaly/compiler';\n\nexport type TriageSignal = 'divergent' | 'collision' | 'echo' | 'digits' | 'code' | 'url';\n\nexport interface TriageReason {\n signal: TriageSignal;\n text: string;\n}\n\nexport type TriageResult = Record<string, TriageReason[]>;\n\n// check() already caught the structure, so these only point at the few worth reading first.\nexport interface TriageInput {\n source: Catalog;\n target: Catalog;\n reviewed: ReviewedLocale[];\n}\n\n// the locale rides with the catalog because a reason that cannot name the language explains nothing\nexport interface ReviewedLocale {\n locale: string;\n catalog: Catalog;\n}\n\nconst DIGITS = /\\d[\\d.,]*/g;\nconst URLS = /https?:\\/\\/[^\\s<)\"']+/g;\nconst CODE = /<code>([^<]+)<\\/code>/g;\n\nfunction bag(text: string, re: RegExp, normalize?: (v: string) => string): string[] {\n const found = text.match(re) ?? [];\n return (normalize ? found.map(normalize) : found).sort();\n}\n\nfunction same(a: string[], b: string[]): boolean {\n return a.length === b.length && a.every((v, i) => v === b[i]);\n}\n\n// A number or a url at the end of a clause takes the punctuation with it, and that is not a signal.\nconst trimPunct = (v: string) => v.replace(/[.,;:!?)\\]]+$/, '');\n// A separator is idiomatic (3.09 is 3,09 in Spanish); canonizing keeps 1.5 apart from 15.\nconst bareDigits = (v: string) => trimPunct(v).replaceAll(/[.,]/g, '.');\n\nfunction push(out: TriageResult, key: string, reason: TriageReason): void {\n (out[key] ??= []).push(reason);\n}\n\n// the column is three short lines, so a reason quotes the evidence and never the whole message\nfunction clip(text: string, max = 42): string {\n return text.length > max ? `${text.slice(0, max - 1)}…` : text;\n}\n\nconst quote = (text: string, max?: number) => `\"${clip(text, max)}\"`;\n\n// what the source has and the translation does not, which is the half worth naming\nfunction lost(from: string[], to: string[]): string[] {\n const rest = [...to];\n const out: string[] = [];\n for (const item of from) {\n const at = rest.indexOf(item);\n if (at < 0) out.push(item);\n else rest.splice(at, 1);\n }\n return out;\n}\n\n// \"the numbers moved\" is a label; naming the one that moved is what saves the reader a diff\nfunction movedText(kind: string, from: string[], to: string[]): string {\n const gone = lost(from, to);\n const added = lost(to, from);\n if (gone.length && added.length) {\n return `the source ${kind} ${quote(gone[0]!)}, this one ${kind} ${quote(added[0]!)}`;\n }\n if (gone.length) return `the source ${kind} ${quote(gone[0]!)} and this one does not`;\n return `this one ${kind} ${quote(added[0]!)} and the source does not`;\n}\n\nexport function triage({ source, target, reviewed }: TriageInput): TriageResult {\n // null-prototype: a source text of \"__proto__\" or \"toString\" is a string, not a member of Object\n const out: TriageResult = Object.create(null);\n const keys = Object.keys(source);\n\n // One source text that two humans rendered two ways is ambiguous, so the machine had no chance.\n const bySource: Record<string, string[]> = Object.create(null);\n for (const key of keys) {\n const text = source[key]?.trim();\n if (text) (bySource[text] ??= []).push(key);\n }\n for (const [text, group] of Object.entries(bySource)) {\n if (group.length < 2) continue;\n\n // the first reviewed locale that disagreed with itself, and the two renderings it used\n let split: { locale: string; first: string; second: string } | undefined;\n for (const { locale, catalog } of reviewed) {\n const seen = [...new Set(group.map((k) => catalog[k]?.trim() ?? '').filter(Boolean))];\n if (seen.length > 1) {\n split = { locale, first: seen[0]!, second: seen[1]! };\n break;\n }\n }\n if (split) {\n for (const key of group) {\n push(out, key, {\n signal: 'divergent',\n text: `${split.locale} already says this two ways: ${quote(split.first, 32)} and ${quote(split.second, 32)}`,\n });\n }\n }\n\n // The same ambiguity seen from the result instead of from the history.\n const targets = new Set(group.map((key) => target[key]?.trim() ?? '').filter(Boolean));\n if (!split && targets.size > 1) {\n for (const key of group) {\n const mine = target[key]?.trim() ?? '';\n const other = group.find((k) => k !== key && (target[k]?.trim() ?? '') !== mine && target[k]?.trim());\n push(out, key, {\n signal: 'collision',\n text: other\n ? `${other} has the same source text and says ${quote(target[other]!.trim())}`\n : `${quote(text)} came back as two different translations`,\n });\n }\n }\n }\n\n for (const key of keys) {\n const src = source[key]?.trim() ?? '';\n const trg = target[key]?.trim() ?? '';\n if (!src || !trg) continue;\n\n // Only the locales with an opinion on this key vote: alone it fires on every do-not-translate.\n const voters = reviewed.filter(({ catalog }) => {\n const value = catalog[key]?.trim();\n return value !== undefined && value !== '';\n });\n if (trg === src && voters.length > 0 && voters.every(({ catalog }) => catalog[key]?.trim() !== src)) {\n const names = voters.map(({ locale }) => locale).join(' and ');\n push(out, key, { signal: 'echo', text: `still the source text, and ${names} did translate it` });\n }\n\n // a translation that adds a number is idiomatic (\"refresh\" is \"F5\"); one that changes it is not\n const srcDigits = bag(src, DIGITS, bareDigits);\n const trgDigits = bag(trg, DIGITS, bareDigits);\n if (srcDigits.length > 0 && !same(srcDigits, trgDigits)) {\n push(out, key, { signal: 'digits', text: movedText('says', srcDigits, trgDigits) });\n }\n const srcUrls = bag(src, URLS, trimPunct);\n const trgUrls = bag(trg, URLS, trimPunct);\n if (!same(srcUrls, trgUrls)) {\n push(out, key, { signal: 'url', text: movedText('links to', srcUrls, trgUrls) });\n }\n const srcCode = bag(src, CODE);\n const trgCode = bag(trg, CODE);\n if (!same(srcCode, trgCode)) {\n push(out, key, { signal: 'code', text: movedText('shows the code', srcCode, trgCode) });\n }\n }\n\n return out;\n}\n","import { existsSync } from 'node:fs';\nimport { relative } from 'node:path';\nimport {\n check,\n clearDrafts,\n collectOrigins,\n doctor,\n effectiveDrafts,\n extractProject,\n loadDrafts,\n markDrafts,\n readCatalog,\n saveDrafts,\n status,\n targetLocales,\n validateMessage,\n validatePair,\n writeCatalog,\n} from '@verbaly/compiler';\nimport { DRAFTS_FILE } from '@verbaly/compiler';\nimport type {\n Catalog,\n Catalogs,\n CheckResult,\n DoctorResult,\n Drafts,\n ResolvedConfig,\n StatusResult,\n} from '@verbaly/compiler';\nimport { badRequest } from './http';\nimport { triage, type TriageResult } from './triage';\n\nexport interface StudioProblem {\n scope: string;\n message: string;\n}\n\nexport interface StudioState {\n root: string;\n dir: string;\n sourceLocale: string;\n locales: string[];\n scanning: boolean;\n catalogs: Catalogs;\n origins: Record<string, string[]>;\n status: StatusResult;\n check: CheckResult;\n drafts: Drafts;\n triage: Record<string, TriageResult>;\n problems: StudioProblem[];\n}\n\n// A locale nobody can parse must not take the panel down: Studio is what you open to fix it.\nfunction readAll(cfg: ResolvedConfig): { catalogs: Catalogs; problems: StudioProblem[] } {\n const catalogs: Catalogs = {};\n const problems: StudioProblem[] = [];\n // an empty green panel and a wrong --root look identical, and only this line tells them apart\n if (!existsSync(cfg.dir)) {\n const dir = relative(cfg.root, cfg.dir).replaceAll('\\\\', '/');\n problems.push({ scope: dir, message: '[verbaly] no catalogs here, run npx verbaly init or check --root' });\n }\n for (const locale of cfg.locales) {\n try {\n catalogs[locale] = readCatalog(cfg, locale);\n } catch (error) {\n catalogs[locale] = {};\n problems.push({\n scope: locale,\n message: error instanceof Error ? error.message : String(error),\n });\n }\n }\n return { catalogs, problems };\n}\n\n// A sidecar nobody can parse is the other file Studio exists to fix, so it degrades like a catalog.\nfunction readDrafts(cfg: ResolvedConfig): { drafts: Drafts; problems: StudioProblem[] } {\n try {\n return { drafts: loadDrafts(cfg), problems: [] };\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n return { drafts: {}, problems: [{ scope: DRAFTS_FILE, message }] };\n }\n}\n\n// check() reports the files a key was used in as absolute paths, origins already do not.\nfunction relativize(cfg: ResolvedConfig, result: CheckResult): CheckResult {\n const rel = (file: string): string => relative(cfg.root, file).replaceAll('\\\\', '/');\n return {\n ...result,\n unknown: result.unknown.map((entry) => ({ ...entry, files: entry.files.map(rel) })),\n extra: result.extra.map((entry) => ({ ...entry, files: entry.files.map(rel) })),\n };\n}\n\n// One shot and unpaginated: the biggest catalog we know is past 1600 messages, instant locally.\nexport async function buildState(cfg: ResolvedConfig): Promise<StudioState> {\n const { catalogs, problems } = readAll(cfg);\n const registry = await extractProject(cfg);\n const sidecar = readDrafts(cfg);\n problems.push(...sidecar.problems);\n const live = effectiveDrafts(sidecar.drafts, catalogs);\n const source = (catalogs[cfg.sourceLocale] ?? {}) as Catalog;\n const targets = targetLocales(cfg);\n\n for (const { file, message } of registry.parseErrors()) {\n problems.push({ scope: relative(cfg.root, file).replaceAll('\\\\', '/'), message });\n }\n\n const perLocale: Record<string, TriageResult> = {};\n for (const locale of targets) {\n perLocale[locale] = triage({\n source,\n target: (catalogs[locale] ?? {}) as Catalog,\n // a locale still carrying drafts has not been reviewed, so it cannot be the control\n reviewed: targets\n .filter((other) => other !== locale && !(live[other] ?? []).length)\n .map((other) => ({ locale: other, catalog: (catalogs[other] ?? {}) as Catalog })),\n });\n }\n\n return {\n root: cfg.root,\n dir: cfg.dir,\n sourceLocale: cfg.sourceLocale,\n locales: cfg.locales,\n // with include: [] nothing is scanned, so an empty origins map is off, not \"none found\"\n scanning: cfg.include.length > 0,\n catalogs,\n origins: await collectOrigins(cfg, registry),\n status: status(cfg, catalogs, registry, live),\n check: relativize(cfg, check(cfg, catalogs, registry)),\n drafts: live,\n triage: perLocale,\n problems,\n };\n}\n\nexport function health(cfg: ResolvedConfig): Promise<DoctorResult> {\n return doctor(cfg);\n}\n\nexport interface WriteMessageResult {\n locale: string;\n key: string;\n clearedDraft: boolean;\n}\n\n// A human wrote it, so it is reviewed: the rule import already applies in run.ts:447.\nexport function writeMessage(\n cfg: ResolvedConfig,\n locale: string,\n key: string,\n text: string,\n): WriteMessageResult {\n if (locale === cfg.sourceLocale) {\n throw badRequest('the source text lives in your code, Studio never edits it');\n }\n if (!cfg.locales.includes(locale)) {\n throw badRequest(`${locale} is not one of this project's locales`);\n }\n const source = readCatalog(cfg, cfg.sourceLocale);\n // hasOwn, not `in`: `in` walks the prototype, so toString and __proto__ would pass this guard\n if (!Object.hasOwn(source, key)) {\n throw badRequest(`\"${key}\" is not in the source catalog, so Studio will not create it`);\n }\n // '' is untranslated everywhere else, so whitespace is that and not a translation to validate\n const value = text.trim() ? text : '';\n // the same two calls check() makes: validatePair alone misses a plural block with no other case\n const issues = value\n ? [...validateMessage(value, locale), ...validatePair(source[key] as string, value)]\n : [];\n const errors = issues.filter((issue) => issue.severity === 'error');\n if (errors.length) {\n throw badRequest(`${errors.map((issue) => issue.message).join('; ')}`);\n }\n\n const catalog = { ...readCatalog(cfg, locale), [key]: value };\n writeCatalog(cfg, locale, catalog);\n\n const drafts = loadDrafts(cfg);\n const clearedDraft = (drafts[locale] ?? []).includes(key);\n clearDrafts(drafts, locale, [key]);\n saveDrafts(cfg, drafts);\n return { locale, key, clearedDraft };\n}\n\nexport interface ApproveResult {\n locale: string;\n approved: number;\n keys: string[];\n}\n\n// The sidecar is re-read here: saveDrafts writes every locale, so a stale copy drops another's.\nexport function approve(cfg: ResolvedConfig, locale: string, keys?: string[]): ApproveResult {\n if (!cfg.locales.includes(locale)) {\n throw badRequest(`${locale} is not one of this project's locales`);\n }\n const drafts = loadDrafts(cfg);\n const had = drafts[locale] ?? [];\n // the keys it really cleared, because undo has to put back exactly those and nothing else\n const cleared = keys ? had.filter((key) => keys.includes(key)) : [...had];\n clearDrafts(drafts, locale, keys);\n saveDrafts(cfg, drafts);\n return { locale, approved: cleared.length, keys: cleared };\n}\n\n// Undo is not a nicety here: the flag records who wrote the text, so restoring it restores a fact.\nexport function unapprove(cfg: ResolvedConfig, locale: string, keys: string[]): ApproveResult {\n if (!cfg.locales.includes(locale)) {\n throw badRequest(`${locale} is not one of this project's locales`);\n }\n const drafts = loadDrafts(cfg);\n markDrafts(drafts, locale, keys);\n saveDrafts(cfg, drafts);\n return { locale, approved: keys.length, keys };\n}\n","import { badRequest, HttpError } from './http';\n\nexport type JobState = 'running' | 'done' | 'failed';\n\nexport interface Job {\n id: string;\n kind: 'translate';\n state: JobState;\n total: number;\n done: number;\n locale?: string;\n message?: string;\n result?: unknown;\n written?: number;\n kept?: number;\n}\n\n// One at a time, because two runs over the same catalogs is a race over the same files.\nlet current: Job | null = null;\nlet counter = 0;\n\nexport function running(): Job | null {\n return current?.state === 'running' ? current : null;\n}\n\nexport function read(id: string): Job {\n if (!current || current.id !== id) throw new HttpError(404, `no job called \"${id}\"`);\n return current;\n}\n\nexport function start(kind: Job['kind'], total: number): Job {\n if (running()) throw badRequest('a job is already running, wait for it or read its progress');\n counter += 1;\n current = { id: `${kind}-${counter}`, kind, state: 'running', total, done: 0 };\n return current;\n}\n\n// A batch that failed still finishes: the bar reports work attempted, and message says what broke.\nexport function advance(id: string, keys: number, locale: string, error?: string): void {\n if (current?.id !== id) return;\n current.done = Math.min(current.done + keys, current.total);\n current.locale = locale;\n if (error) current.message = error;\n}\n\n// A run that writes part of its work and then fails is a success for what it wrote, so both land.\nexport function finish(id: string, state: 'done' | 'failed', patch: Partial<Job> = {}): void {\n if (current?.id === id) Object.assign(current, patch, { state });\n}\n","import {\n collectOrigins,\n counted,\n extractProject,\n loadCatalogs,\n mergeTranslations,\n resolveProvider,\n syncCatalogs,\n translateCatalogs,\n writeCatalog,\n writeDts,\n markDrafts,\n loadDrafts,\n saveDrafts,\n} from '@verbaly/compiler';\nimport type { ResolvedConfig } from '@verbaly/compiler';\nimport { badRequest } from './http';\nimport { advance, finish, start, type Job } from './jobs';\n\nexport interface ExtractResult {\n added: Record<string, string[]>;\n // only the source locale counts as new text: a target gains a blank for every gap it had\n found: number;\n messages: number;\n}\n\n// Local, fast and free, so it answers in the same request instead of becoming a job.\nexport async function runExtract(cfg: ResolvedConfig): Promise<ExtractResult> {\n if (!cfg.include.length) {\n throw badRequest('source scanning is off (include: []), so there is nothing to extract');\n }\n const registry = await extractProject(cfg);\n const catalogs = loadCatalogs(cfg);\n const result = syncCatalogs(cfg, catalogs, registry);\n for (const locale of cfg.locales) writeCatalog(cfg, locale, catalogs[locale] ?? {});\n // the types come from the source catalog; writeDts skips them when the project set dts: false\n writeDts(cfg, catalogs[cfg.sourceLocale] ?? {});\n const found = result.added[cfg.sourceLocale]?.length ?? 0;\n return { added: result.added, found, messages: registry.messages().size };\n}\n\nexport interface Plan {\n pending: Record<string, string[]>;\n total: number;\n}\n\n// translate is the one command that spends money and minutes, so the panel always shows the bill.\nexport async function planTranslate(cfg: ResolvedConfig, locales?: string[]): Promise<Plan> {\n const provider = await resolveProvider(cfg);\n const catalogs = loadCatalogs(cfg);\n const result = await translateCatalogs(cfg, catalogs, provider, { locales, dryRun: true });\n const total = Object.values(result.pending).reduce((n, keys) => n + keys.length, 0);\n return { pending: result.pending, total };\n}\n\nexport async function startTranslate(cfg: ResolvedConfig, locales?: string[]): Promise<Job> {\n const provider = await resolveProvider(cfg);\n // the job carries its own denominator, so progress reads the same whether or not you asked first\n const plan = await planTranslate(cfg, locales);\n const job = start('translate', plan.total);\n const id = job.id;\n\n // the run outlives this request on purpose: the client asks the job how it is going\n void (async () => {\n try {\n const catalogs = loadCatalogs(cfg);\n const result = await translateCatalogs(cfg, catalogs, provider, {\n locales,\n batchSize: cfg.translate.batchSize,\n concurrency: cfg.translate.concurrency,\n retries: cfg.translate.retries,\n origins: await collectOrigins(cfg),\n onProgress: (p) => advance(id, p.keys, p.locale, p.error),\n });\n // what a machine wrote stays a draft: the panel does not get to change that rule\n const drafts = loadDrafts(cfg);\n let written = 0;\n let kept = 0;\n for (const [locale, keys] of Object.entries(result.translated)) {\n const landed = mergeTranslations(cfg, locale, catalogs[locale] ?? {}, keys);\n markDrafts(drafts, locale, landed);\n written += landed.length;\n kept += keys.length - landed.length;\n }\n saveDrafts(cfg, drafts);\n // on a failure the panel shows only this line, so it says what landed and what did not\n const failed = result.failed[0];\n finish(id, failed ? 'failed' : 'done', {\n result,\n written,\n kept,\n message: failed\n ? `${counted(written, 'message')} translated, but ${counted(result.failed.length, 'batch', 'batches')} did not answer (${failed.error}): run it again for the rest`\n : undefined,\n });\n } catch (error) {\n finish(id, 'failed', { message: error instanceof Error ? error.message : String(error) });\n }\n })();\n\n return job;\n}\n","import { execFile, spawn } from 'node:child_process';\nimport { promisify } from 'node:util';\nimport { join, relative } from 'node:path';\nimport { parseCatalog, type Catalog, type ResolvedConfig } from '@verbaly/compiler';\n\nconst run = promisify(execFile);\n\nexport interface Commit {\n sha: string;\n author: string;\n date: string;\n subject: string;\n}\n\n// one field separator that no subject can contain, written as the escape and never as the byte\nconst FIELD = '\\u001f';\nconst FORMAT = `--format=%H${FIELD}%an${FIELD}%aI${FIELD}%s`;\n\n// the full sha is what git resolves without ambiguity, and seven characters is what a card shows\nexport function parseFullLog(stdout: string): Commit[] {\n const commits: Commit[] = [];\n for (const line of stdout.split('\\n')) {\n const [sha, author, date, ...rest] = line.split(FIELD);\n // a subject holds anything but a newline, so it is the last field and is rejoined\n if (!sha || !author || !date || !rest.length) continue;\n commits.push({ sha, author, date, subject: rest.join(FIELD) });\n }\n return commits;\n}\n\n// split out so the shape of a log line is tested without a repository and without writing one\nexport function parseLog(stdout: string): Commit[] {\n return parseFullLog(stdout).map((commit) => ({ ...commit, sha: commit.sha.slice(0, 7) }));\n}\n\n// git is asked, never assumed: no repository and no git both mean there is no history to show\nexport async function catalogHistory(cfg: ResolvedConfig, limit = 5): Promise<Commit[]> {\n const dir = relative(cfg.root, cfg.dir).replaceAll('\\\\', '/') || '.';\n try {\n const { stdout } = await run('git', ['log', `-n${limit}`, FORMAT, '--', dir], {\n cwd: cfg.root,\n timeout: 3000,\n windowsHide: true,\n });\n return parseLog(stdout);\n } catch {\n return [];\n }\n}\n\nexport interface Revision {\n commit: Commit;\n now: Catalog | undefined;\n before: Catalog | undefined;\n}\n\n// blame measured 26% wrong on verbaly-web: a sibling's comma rewrites a line, a value does not\nexport function attribute(\n revisions: Revision[],\n owners = new Map<string, Commit>(),\n): Map<string, Commit> {\n for (const { commit, now, before } of revisions) {\n if (!now || !before) continue;\n for (const key of new Set([...Object.keys(now), ...Object.keys(before)])) {\n if (!owners.has(key) && now[key] !== before[key]) owners.set(key, commit);\n }\n }\n return owners;\n}\n\n// a key can move in more than one catalog, so the answer is the newest of the per-file answers\nexport function mergeOwners(maps: Map<string, Commit>[]): Map<string, Commit> {\n const merged = new Map<string, Commit>();\n for (const map of maps) {\n for (const [key, commit] of map) {\n const held = merged.get(key);\n if (!held || Date.parse(commit.date) > Date.parse(held.date)) merged.set(key, commit);\n }\n }\n return merged;\n}\n\nconst BLOB = /^[0-9a-f]{40} \\w+ (\\d+)$/;\n\n// git cat-file --batch answers every revision in one process: a header line, then that many bytes\nfunction readBlobs(\n root: string,\n requests: string[],\n take: (index: number, body: string | null, blob: string) => void,\n): Promise<boolean> {\n return new Promise((resolve) => {\n const child = spawn('git', ['cat-file', '--batch'], { cwd: root, windowsHide: true });\n // a killed batch leaves a half map, and a half map answers \"never committed\" for the rest\n const stop = setTimeout(() => {\n ok = false;\n child.kill();\n }, 20000);\n let pending: Buffer = Buffer.alloc(0);\n let index = 0;\n let ok = true;\n const fail = () => {\n ok = false;\n };\n child.on('error', fail);\n child.stdin.on('error', fail);\n child.stdout.on('data', (chunk: Buffer) => {\n pending = pending.length ? Buffer.concat([pending, chunk]) : chunk;\n for (;;) {\n const end = pending.indexOf(10);\n if (end < 0) break;\n const header = pending.toString('utf8', 0, end);\n const blob = BLOB.exec(header);\n // anything but a blob header is a revision git could not resolve, which is an answer too\n if (!blob) {\n take(index++, null, '');\n pending = pending.subarray(end + 1);\n continue;\n }\n const size = Number(blob[1]);\n if (pending.length < end + size + 2) break;\n take(index++, pending.toString('utf8', end + 1, end + 1 + size), header.slice(0, 40));\n pending = pending.subarray(end + size + 2);\n }\n });\n child.on('close', () => {\n clearTimeout(stop);\n resolve(ok);\n });\n child.stdin.end(requests.join('\\n') + '\\n');\n });\n}\n\nexport interface KeyHistory {\n owners: Map<string, Commit>;\n committed: Set<string>;\n ok: boolean;\n}\n\n// the window a local tool is willing to read: past this, the answer is \"older than what I read\"\nconst WINDOW = 200;\n\nasync function walkFile(root: string, file: string, limit: number) {\n const { stdout } = await run('git', ['log', `-n${limit}`, FORMAT, '--', file], {\n cwd: root,\n timeout: 5000,\n windowsHide: true,\n maxBuffer: 8_000_000,\n });\n const commits = parseFullLog(stdout);\n if (!commits.length) return { owners: new Map<string, Commit>(), head: [] as string[] };\n\n const owners = new Map<string, Commit>();\n let head: string[] = [];\n let waiting: Catalog | undefined;\n // only two flat catalogs are ever live, and the pair shares a blob, so one entry caches it\n let last: { blob: string; flat: Catalog | undefined } | null = null;\n const parse = (body: string | null, blob: string): Catalog | undefined => {\n if (body === null) return {};\n if (last && last.blob === blob) return last.flat;\n const flat = parseCatalog(body);\n last = { blob, flat };\n return flat;\n };\n\n const whole = await readBlobs(\n root,\n commits.flatMap((commit) => [`${commit.sha}:${file}`, `${commit.sha}^:${file}`]),\n (index, body, blob) => {\n const flat = parse(body, blob);\n if (index % 2 === 0) {\n waiting = flat;\n if (index === 0 && flat) head = Object.keys(flat);\n return;\n }\n const commit = commits[(index - 1) / 2];\n if (commit) attribute([{ commit, now: waiting, before: flat }], owners);\n waiting = undefined;\n },\n );\n if (!whole) throw new Error('[verbaly] git could not read every revision of this catalog');\n return { owners, head };\n}\n\n// The whole map at once, because a per-key query costs a git process and every row has a menu.\nexport async function catalogKeyHistory(\n cfg: ResolvedConfig,\n limit = WINDOW,\n): Promise<KeyHistory> {\n const maps: Map<string, Commit>[] = [];\n const committed = new Set<string>();\n let ok = false;\n for (const locale of cfg.locales) {\n const file = relative(cfg.root, join(cfg.dir, `${locale}.json`)).replaceAll('\\\\', '/');\n try {\n const walked = await walkFile(cfg.root, file, limit);\n maps.push(walked.owners);\n for (const key of walked.head) committed.add(key);\n ok = true;\n } catch {\n // one unreadable locale is not the end of the walk: the others still answer for their keys\n }\n }\n return { owners: mergeOwners(maps), committed, ok };\n}\n\nexport interface KeyCommit {\n key: string;\n commit: Commit | null;\n reason?: 'nogit' | 'uncommitted' | 'older';\n}\n\nconst walked = new Map<string, { head: string; history: KeyHistory }>();\n\nasync function headSha(root: string): Promise<string> {\n try {\n const { stdout } = await run('git', ['rev-parse', 'HEAD'], {\n cwd: root,\n timeout: 3000,\n windowsHide: true,\n });\n return stdout.trim();\n } catch {\n return '';\n }\n}\n\n// HEAD moving is the only thing that can change an answer, so it is what invalidates the walk\nasync function keyHistory(cfg: ResolvedConfig): Promise<KeyHistory> {\n const head = await headSha(cfg.root);\n const held = walked.get(cfg.root);\n if (held && held.head === head) return held.history;\n const history = await catalogKeyHistory(cfg);\n walked.set(cfg.root, { head, history });\n return history;\n}\n\nexport async function keyCommit(cfg: ResolvedConfig, key: string): Promise<KeyCommit> {\n const history = await keyHistory(cfg);\n if (!history.ok) return { key, commit: null, reason: 'nogit' };\n const commit = history.owners.get(key);\n // seven characters is what a person reads and pastes, and the walk needed the full one\n if (commit) return { key, commit: { ...commit, sha: commit.sha.slice(0, 7) } };\n return { key, commit: null, reason: history.committed.has(key) ? 'older' : 'uncommitted' };\n}\n","import { existsSync, readFileSync, statSync } from 'node:fs';\nimport { join, normalize, resolve } from 'node:path';\nimport { fileURLToPath } from 'node:url';\n\n// the built panel sits beside the compiled server, so it is found relative to this file, never cwd\nconst UI = resolve(fileURLToPath(new URL('.', import.meta.url)), 'ui');\n\nconst TYPES: Record<string, string> = {\n '.html': 'text/html; charset=utf-8',\n '.js': 'text/javascript; charset=utf-8',\n '.css': 'text/css; charset=utf-8',\n '.svg': 'image/svg+xml',\n '.woff2': 'font/woff2',\n '.json': 'application/json; charset=utf-8',\n '.ico': 'image/x-icon',\n};\n\nexport interface Asset {\n body: Buffer;\n type: string;\n}\n\nexport const uiBuilt = (): boolean => existsSync(join(UI, 'index.html'));\n\n// A path is resolved and then checked to still be inside UI: ../ and an absolute one both land out.\nexport function readAsset(pathname: string): Asset | undefined {\n const wanted = pathname === '/' ? '/index.html' : pathname;\n const target = resolve(join(UI, normalize(wanted)));\n if (target !== UI && !target.startsWith(UI + (process.platform === 'win32' ? '\\\\' : '/'))) {\n return undefined;\n }\n if (!existsSync(target) || !statSync(target).isFile()) return undefined;\n const dot = target.lastIndexOf('.');\n return {\n body: readFileSync(target),\n type: TYPES[target.slice(dot).toLowerCase()] ?? 'application/octet-stream',\n };\n}\n","import { randomBytes, timingSafeEqual } from 'node:crypto';\n\nexport interface GuardOptions {\n port: number;\n token: string;\n}\n\n// A local server that writes files is reachable from any page the browser has open.\nexport function newToken(): string {\n return randomBytes(9).toString('base64url');\n}\n\n// Only the two names that resolve to this machine. A DNS rebind arrives with a foreign Host.\nexport function hostAllowed(host: string | undefined, port: number): boolean {\n // a browser omits the port when it is the scheme default, so :80 would 403 every real request\n const names = port === 80 ? ['127.0.0.1', 'localhost'] : [];\n return [`127.0.0.1:${port}`, `localhost:${port}`, ...names].includes(host ?? '');\n}\n\n// No Origin at all is a same-origin navigation or a curl, both fine; a foreign one never is.\nexport function originAllowed(origin: string | undefined, port: number): boolean {\n if (origin === undefined) return true;\n return origin === `http://127.0.0.1:${port}` || origin === `http://localhost:${port}`;\n}\n\n// Compared in constant time so the token cannot be guessed one character at a time.\nexport function tokenMatches(given: string | null, expected: string): boolean {\n if (given === null) return false;\n const a = Buffer.from(given);\n const b = Buffer.from(expected);\n return a.length === b.length && timingSafeEqual(a, b);\n}\n\nexport interface GuardVerdict {\n ok: boolean;\n status: number;\n reason: string;\n}\n\nconst PASS: GuardVerdict = { ok: true, status: 200, reason: '' };\n\nexport function guardRequest(\n headers: { host?: string; origin?: string },\n url: URL,\n { port, token }: GuardOptions,\n): GuardVerdict {\n if (!hostAllowed(headers.host, port)) {\n return { ok: false, status: 403, reason: '[verbaly] unexpected Host header' };\n }\n if (!originAllowed(headers.origin, port)) {\n return { ok: false, status: 403, reason: '[verbaly] cross-origin request' };\n }\n if (!tokenMatches(url.searchParams.get('t'), token)) {\n return { ok: false, status: 401, reason: '[verbaly] missing or wrong token, open the printed url' };\n }\n return PASS;\n}\n","import { createServer, type IncomingMessage, type Server, type ServerResponse } from 'node:http';\nimport type { AddressInfo } from 'node:net';\nimport type { ResolvedConfig } from '@verbaly/compiler';\nimport { approve, buildState, health, unapprove, writeMessage } from './api';\nimport { planTranslate, runExtract, startTranslate } from './actions';\nimport { read as readJob, running } from './jobs';\nimport { catalogHistory, keyCommit } from './history';\nimport { readAsset, uiBuilt } from './assets';\nimport { guardRequest, newToken } from './guard';\nimport { HttpError, badRequest, scrub } from './http';\n\nexport interface StudioServer {\n server: Server;\n port: number;\n token: string;\n url: string;\n close: () => Promise<void>;\n}\n\nexport interface StartOptions {\n port?: number;\n token?: string;\n}\n\nconst DEFAULT_PORT = 4747;\nconst MAX_BODY = 1_000_000;\n\nfunction send(res: ServerResponse, status: number, body: unknown): void {\n res.writeHead(status, {\n 'content-type': 'application/json; charset=utf-8',\n 'cache-control': 'no-store',\n });\n res.end(JSON.stringify(body));\n}\n\nasync function readJson(req: IncomingMessage): Promise<Record<string, unknown>> {\n const chunks: Buffer[] = [];\n let size = 0;\n for await (const chunk of req) {\n size += (chunk as Buffer).length;\n if (size > MAX_BODY) throw new HttpError(413, 'request body too large');\n chunks.push(chunk as Buffer);\n }\n if (!chunks.length) return {};\n let parsed: unknown;\n try {\n parsed = JSON.parse(Buffer.concat(chunks).toString('utf8'));\n } catch {\n throw badRequest('body is not valid JSON');\n }\n // null and an array both pass typeof 'object', and reading .text off them is a 500\n if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {\n throw badRequest('body must be a JSON object');\n }\n return parsed as Record<string, unknown>;\n}\n\nfunction segment(raw: string | undefined, what: string): string {\n let value: string;\n try {\n value = decodeURIComponent(raw ?? '');\n } catch {\n throw badRequest(`the ${what} in the url is not valid percent-encoding`);\n }\n if (!value) throw badRequest(`the url is missing a ${what}`);\n return value;\n}\n\n// One page, JSON on every route: a framework would only add a dependency to a local tool.\nexport function createStudioApp(cfg: ResolvedConfig, port: number, token: string) {\n return async function handle(req: IncomingMessage, res: ServerResponse): Promise<void> {\n // The url is built inside the try: `GET //` throws, and out here that would kill the process.\n try {\n let url: URL;\n try {\n url = new URL(req.url ?? '/', `http://127.0.0.1:${port}`);\n } catch {\n throw badRequest('the request target is not a valid url');\n }\n\n const api = url.pathname.startsWith('/api/');\n const verdict = guardRequest({ host: req.headers.host, origin: req.headers.origin }, url, {\n port,\n token,\n });\n // the panel files are the published npm package: only the project data needs a token\n if (!verdict.ok && (api || verdict.status !== 401)) {\n send(res, verdict.status, { error: verdict.reason });\n return;\n }\n\n if (!api && req.method === 'GET') {\n const asset = readAsset(url.pathname);\n if (asset) {\n res.writeHead(200, { 'content-type': asset.type, 'cache-control': 'no-store' });\n res.end(asset.body);\n return;\n }\n if (url.pathname === '/' && !uiBuilt()) {\n throw new HttpError(500, 'the panel is not in this install, so only /api/* answers here');\n }\n }\n\n if (req.method === 'GET' && url.pathname === '/api/state') {\n send(res, 200, await buildState(cfg));\n return;\n }\n if (req.method === 'GET' && url.pathname === '/api/history') {\n send(res, 200, { commits: await catalogHistory(cfg) });\n return;\n }\n if (req.method === 'GET' && url.pathname.startsWith('/api/commit/')) {\n const key = segment(url.pathname.slice('/api/commit/'.length), 'key');\n send(res, 200, await keyCommit(cfg, key));\n return;\n }\n if (req.method === 'GET' && url.pathname === '/api/health') {\n send(res, 200, await health(cfg));\n return;\n }\n if (req.method === 'PUT' && url.pathname.startsWith('/api/message/')) {\n const rest = url.pathname.slice('/api/message/'.length).split('/');\n const locale = segment(rest[0], 'locale');\n const key = segment(rest.slice(1).join('/'), 'key');\n const body = await readJson(req);\n if (typeof body.text !== 'string') throw badRequest('body needs a \"text\" string');\n send(res, 200, writeMessage(cfg, locale, key, body.text));\n return;\n }\n if (req.method === 'POST' && url.pathname === '/api/approve') {\n const body = await readJson(req);\n if (typeof body.locale !== 'string') throw badRequest('body needs a \"locale\" string');\n const keys = Array.isArray(body.keys)\n ? body.keys.filter((k): k is string => typeof k === 'string')\n : undefined;\n if (body.undo === true) {\n if (!keys?.length) throw badRequest('undoing needs the \"keys\" it should put back');\n send(res, 200, unapprove(cfg, body.locale, keys));\n return;\n }\n send(res, 200, approve(cfg, body.locale, keys));\n return;\n }\n if (req.method === 'POST' && url.pathname === '/api/extract') {\n send(res, 200, await runExtract(cfg));\n return;\n }\n if (req.method === 'GET' && url.pathname === '/api/translate') {\n const locales = url.searchParams.getAll('locale');\n send(res, 200, await planTranslate(cfg, locales.length ? locales : undefined));\n return;\n }\n if (req.method === 'POST' && url.pathname === '/api/translate') {\n const body = await readJson(req);\n const locales = Array.isArray(body.locales)\n ? body.locales.filter((l): l is string => typeof l === 'string')\n : undefined;\n send(res, 200, await startTranslate(cfg, locales?.length ? locales : undefined));\n return;\n }\n // a reload loses the id, so the panel asks what is running rather than losing the bar\n if (req.method === 'GET' && url.pathname === '/api/job') {\n send(res, 200, running());\n return;\n }\n if (req.method === 'GET' && url.pathname.startsWith('/api/job/')) {\n send(res, 200, readJob(segment(url.pathname.slice('/api/job/'.length), 'job id')));\n return;\n }\n\n send(res, 404, { error: `[verbaly] no route for ${req.method} ${url.pathname}` });\n } catch (error) {\n const status = error instanceof HttpError ? error.status : 500;\n const message = error instanceof Error ? error.message : String(error);\n send(res, status, { error: scrub(message, cfg.root) });\n }\n };\n}\n\n// A busy port climbs instead of failing: this is a local tool, not a service on a fixed address.\nfunction listen(server: Server, port: number, attemptsLeft: number): Promise<number> {\n return new Promise((resolve, reject) => {\n const onError = (error: NodeJS.ErrnoException) => {\n if (error.code === 'EADDRINUSE' && attemptsLeft > 0) {\n server.removeListener('error', onError);\n resolve(listen(server, port + 1, attemptsLeft - 1));\n return;\n }\n reject(error);\n };\n server.once('error', onError);\n server.listen(port, '127.0.0.1', () => {\n server.removeListener('error', onError);\n // port 0 asks the system to pick, so the bound address is the only place the answer is\n const bound = server.address() as AddressInfo | string | null;\n resolve(typeof bound === 'object' && bound !== null ? bound.port : port);\n });\n });\n}\n\nexport async function startStudio(\n cfg: ResolvedConfig,\n options: StartOptions = {},\n): Promise<StudioServer> {\n const token = options.token ?? newToken();\n const server = createServer();\n // The handler is attached before listen so no request can arrive without one.\n let port = options.port ?? DEFAULT_PORT;\n server.on('request', (req, res) => void createStudioApp(cfg, port, token)(req, res));\n port = await listen(server, port, 20);\n\n return {\n server,\n port,\n token,\n url: `http://127.0.0.1:${port}/?t=${token}`,\n close: () =>\n new Promise<void>((resolve, reject) =>\n server.close((error) => (error ? reject(error) : resolve())),\n ),\n };\n}\n","#!/usr/bin/env node\nimport { parseArgs } from 'node:util';\nimport { counted, formatCliError, loadConfig } from '@verbaly/compiler';\nimport { buildState } from './api';\nimport { startStudio } from './server';\n\ntry {\n const { values } = parseArgs({\n options: {\n root: { type: 'string' },\n port: { type: 'string' },\n json: { type: 'boolean' },\n },\n });\n const port = values.port === undefined ? undefined : Number(values.port);\n if (port !== undefined && !Number.isInteger(port)) {\n throw new Error(`[verbaly] --port needs a number, got \"${values.port}\"`);\n }\n const cfg = await loadConfig(values.root ?? process.cwd());\n\n // --json is the state the interface reads, on stdout, before any interface exists.\n if (values.json) {\n console.log(JSON.stringify(await buildState(cfg), null, 2));\n } else {\n const studio = await startStudio(cfg, { port });\n const state = await buildState(cfg);\n const missing = state.status.locales.reduce((n, l) => n + (l.total - l.translated), 0);\n const drafts = Object.values(state.drafts).reduce((n, keys) => n + keys.length, 0);\n console.log(`\\n Verbaly Studio ${studio.url}`);\n console.log(` Project ${cfg.root}`);\n console.log(\n ` Catalog ${counted(state.status.messages, 'message')} · ` +\n `${missing} untranslated · ${counted(drafts, 'draft')}`,\n );\n for (const problem of state.problems) console.log(` ! ${problem.scope}: ${problem.message}`);\n console.log('\\n Ctrl+C to stop\\n');\n }\n} catch (error) {\n console.error(formatCliError(error));\n process.exitCode = 1;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"cli.js","names":["readJob"],"sources":["../src/http.ts","../src/triage.ts","../src/api.ts","../src/jobs.ts","../src/actions.ts","../src/history.ts","../src/assets.ts","../src/guard.ts","../src/server.ts","../src/cli.ts"],"sourcesContent":["// A client mistake is not a server fault, so the status rides with the error, not a blanket 500.\nexport class HttpError extends Error {\n readonly status: number;\n\n constructor(status: number, message: string) {\n super(message.startsWith('[verbaly]') ? message : `[verbaly] ${message}`);\n this.status = status;\n this.name = 'HttpError';\n }\n}\n\nexport const badRequest = (message: string) => new HttpError(400, message);\n\n// The absolute path of someone's project is not the browser's business.\nexport function scrub(message: string, root: string): string {\n return root ? message.split(root).join('.') : message;\n}\n","import type { Catalog } from '@verbaly/compiler';\n\nexport type TriageSignal = 'divergent' | 'collision' | 'echo' | 'digits' | 'code' | 'url';\n\nexport interface TriageReason {\n signal: TriageSignal;\n text: string;\n}\n\nexport type TriageResult = Record<string, TriageReason[]>;\n\n// check() already caught the structure, so these only point at the few worth reading first.\nexport interface TriageInput {\n source: Catalog;\n target: Catalog;\n reviewed: ReviewedLocale[];\n}\n\n// the locale rides with the catalog because a reason that cannot name the language explains nothing\nexport interface ReviewedLocale {\n locale: string;\n catalog: Catalog;\n}\n\nconst DIGITS = /\\d[\\d.,]*/g;\nconst URLS = /https?:\\/\\/[^\\s<)\"']+/g;\nconst CODE = /<code>([^<]+)<\\/code>/g;\n\nfunction bag(text: string, re: RegExp, normalize?: (v: string) => string): string[] {\n const found = text.match(re) ?? [];\n return (normalize ? found.map(normalize) : found).sort();\n}\n\nfunction same(a: string[], b: string[]): boolean {\n return a.length === b.length && a.every((v, i) => v === b[i]);\n}\n\n// A number or a url at the end of a clause takes the punctuation with it, and that is not a signal.\nconst trimPunct = (v: string) => v.replace(/[.,;:!?)\\]]+$/, '');\n// A separator is idiomatic (3.09 is 3,09 in Spanish); canonizing keeps 1.5 apart from 15.\nconst bareDigits = (v: string) => trimPunct(v).replaceAll(/[.,]/g, '.');\n\nfunction push(out: TriageResult, key: string, reason: TriageReason): void {\n (out[key] ??= []).push(reason);\n}\n\n// the column is three short lines, so a reason quotes the evidence and never the whole message\nfunction clip(text: string, max = 42): string {\n return text.length > max ? `${text.slice(0, max - 1)}…` : text;\n}\n\nconst quote = (text: string, max?: number) => `\"${clip(text, max)}\"`;\n\n// what the source has and the translation does not, which is the half worth naming\nfunction lost(from: string[], to: string[]): string[] {\n const rest = [...to];\n const out: string[] = [];\n for (const item of from) {\n const at = rest.indexOf(item);\n if (at < 0) out.push(item);\n else rest.splice(at, 1);\n }\n return out;\n}\n\n// \"the numbers moved\" is a label; naming the one that moved is what saves the reader a diff\nfunction movedText(kind: string, from: string[], to: string[]): string {\n const gone = lost(from, to);\n const added = lost(to, from);\n if (gone.length && added.length) {\n return `the source ${kind} ${quote(gone[0]!)}, this one ${kind} ${quote(added[0]!)}`;\n }\n if (gone.length) return `the source ${kind} ${quote(gone[0]!)} and this one does not`;\n return `this one ${kind} ${quote(added[0]!)} and the source does not`;\n}\n\nexport function triage({ source, target, reviewed }: TriageInput): TriageResult {\n // null-prototype: a source text of \"__proto__\" or \"toString\" is a string, not a member of Object\n const out: TriageResult = Object.create(null);\n const keys = Object.keys(source);\n\n // One source text that two humans rendered two ways is ambiguous, so the machine had no chance.\n const bySource: Record<string, string[]> = Object.create(null);\n for (const key of keys) {\n const text = source[key]?.trim();\n if (text) (bySource[text] ??= []).push(key);\n }\n for (const [text, group] of Object.entries(bySource)) {\n if (group.length < 2) continue;\n\n // the first reviewed locale that disagreed with itself, and the two renderings it used\n let split: { locale: string; first: string; second: string } | undefined;\n for (const { locale, catalog } of reviewed) {\n const seen = [...new Set(group.map((k) => catalog[k]?.trim() ?? '').filter(Boolean))];\n if (seen.length > 1) {\n split = { locale, first: seen[0]!, second: seen[1]! };\n break;\n }\n }\n if (split) {\n for (const key of group) {\n push(out, key, {\n signal: 'divergent',\n text: `${split.locale} already says this two ways: ${quote(split.first, 32)} and ${quote(split.second, 32)}`,\n });\n }\n }\n\n // The same ambiguity seen from the result instead of from the history.\n const targets = new Set(group.map((key) => target[key]?.trim() ?? '').filter(Boolean));\n if (!split && targets.size > 1) {\n for (const key of group) {\n const mine = target[key]?.trim() ?? '';\n const other = group.find((k) => k !== key && (target[k]?.trim() ?? '') !== mine && target[k]?.trim());\n push(out, key, {\n signal: 'collision',\n text: other\n ? `${other} has the same source text and says ${quote(target[other]!.trim())}`\n : `${quote(text)} came back as two different translations`,\n });\n }\n }\n }\n\n for (const key of keys) {\n const src = source[key]?.trim() ?? '';\n const trg = target[key]?.trim() ?? '';\n if (!src || !trg) continue;\n\n // Only the locales with an opinion on this key vote: alone it fires on every do-not-translate.\n const voters = reviewed.filter(({ catalog }) => {\n const value = catalog[key]?.trim();\n return value !== undefined && value !== '';\n });\n if (trg === src && voters.length > 0 && voters.every(({ catalog }) => catalog[key]?.trim() !== src)) {\n const names = voters.map(({ locale }) => locale).join(' and ');\n push(out, key, { signal: 'echo', text: `still the source text, and ${names} did translate it` });\n }\n\n // a translation that adds a number is idiomatic (\"refresh\" is \"F5\"); one that changes it is not\n const srcDigits = bag(src, DIGITS, bareDigits);\n const trgDigits = bag(trg, DIGITS, bareDigits);\n if (srcDigits.length > 0 && !same(srcDigits, trgDigits)) {\n push(out, key, { signal: 'digits', text: movedText('says', srcDigits, trgDigits) });\n }\n const srcUrls = bag(src, URLS, trimPunct);\n const trgUrls = bag(trg, URLS, trimPunct);\n if (!same(srcUrls, trgUrls)) {\n push(out, key, { signal: 'url', text: movedText('links to', srcUrls, trgUrls) });\n }\n const srcCode = bag(src, CODE);\n const trgCode = bag(trg, CODE);\n if (!same(srcCode, trgCode)) {\n push(out, key, { signal: 'code', text: movedText('shows the code', srcCode, trgCode) });\n }\n }\n\n return out;\n}\n","import { existsSync } from 'node:fs';\nimport { relative } from 'node:path';\nimport {\n check,\n clearDrafts,\n collectOrigins,\n doctor,\n effectiveDrafts,\n extractProject,\n loadDrafts,\n loadState,\n markDrafts,\n readCatalog,\n readState,\n recordTranslations,\n saveDrafts,\n shippedCatalogs,\n status,\n targetLocales,\n validateMessage,\n validatePair,\n writeCatalog,\n} from '@verbaly/compiler';\nimport { STATE_FILE } from '@verbaly/compiler';\nimport type {\n Catalog,\n Catalogs,\n CheckResult,\n DoctorResult,\n Drafts,\n ResolvedConfig,\n StatusResult,\n} from '@verbaly/compiler';\nimport { badRequest } from './http';\nimport { triage, type TriageResult } from './triage';\n\nexport interface StudioProblem {\n scope: string;\n message: string;\n}\n\nexport interface StudioState {\n root: string;\n dir: string;\n sourceLocale: string;\n locales: string[];\n scanning: boolean;\n catalogs: Catalogs;\n origins: Record<string, string[]>;\n status: StatusResult;\n check: CheckResult;\n drafts: Drafts;\n triage: Record<string, TriageResult>;\n problems: StudioProblem[];\n}\n\n// A locale nobody can parse must not take the panel down: Studio is what you open to fix it.\nfunction readAll(cfg: ResolvedConfig): { catalogs: Catalogs; problems: StudioProblem[] } {\n const catalogs: Catalogs = {};\n const problems: StudioProblem[] = [];\n // an empty green panel and a wrong --root look identical, and only this line tells them apart\n if (!existsSync(cfg.dir)) {\n const dir = relative(cfg.root, cfg.dir).replaceAll('\\\\', '/');\n problems.push({ scope: dir, message: '[verbaly] no catalogs here, run npx verbaly init or check --root' });\n }\n for (const locale of cfg.locales) {\n try {\n catalogs[locale] = readCatalog(cfg, locale);\n } catch (error) {\n catalogs[locale] = {};\n problems.push({\n scope: locale,\n message: error instanceof Error ? error.message : String(error),\n });\n }\n }\n return { catalogs, problems };\n}\n\n\n// check() reports the files a key was used in as absolute paths, origins already do not.\nfunction relativize(cfg: ResolvedConfig, result: CheckResult): CheckResult {\n const rel = (file: string): string => relative(cfg.root, file).replaceAll('\\\\', '/');\n return {\n ...result,\n unknown: result.unknown.map((entry) => ({ ...entry, files: entry.files.map(rel) })),\n extra: result.extra.map((entry) => ({ ...entry, files: entry.files.map(rel) })),\n collisions: result.collisions.map((entry) => ({\n ...entry,\n sites: entry.sites.map((at) => ({ ...at, file: rel(at.file) })),\n })),\n divergent: result.divergent.map((entry) => ({\n ...entry,\n code: { ...entry.code, file: rel(entry.code.file) },\n })),\n };\n}\n\n// One shot and unpaginated: the biggest catalog we know is past 1600 messages, instant locally.\nexport async function buildState(cfg: ResolvedConfig): Promise<StudioState> {\n const { catalogs, problems } = readAll(cfg);\n const registry = await extractProject(cfg);\n // a sidecar nobody can parse is the other file Studio is there to fix: it degrades like a catalog\n const { state: sidecar, problem } = readState(cfg);\n if (problem) problems.push({ scope: STATE_FILE, message: problem });\n const live = effectiveDrafts(sidecar.drafts, catalogs);\n const source = (catalogs[cfg.sourceLocale] ?? {}) as Catalog;\n const targets = targetLocales(cfg);\n\n for (const { file, message } of registry.parseErrors()) {\n problems.push({ scope: relative(cfg.root, file).replaceAll('\\\\', '/'), message });\n }\n\n const perLocale: Record<string, TriageResult> = {};\n for (const locale of targets) {\n perLocale[locale] = triage({\n source,\n target: (catalogs[locale] ?? {}) as Catalog,\n // a locale still carrying drafts has not been reviewed, so it cannot be the control\n reviewed: targets\n .filter((other) => other !== locale && !(live[other] ?? []).length)\n .map((other) => ({ locale: other, catalog: (catalogs[other] ?? {}) as Catalog })),\n });\n }\n\n return {\n root: cfg.root,\n dir: cfg.dir,\n sourceLocale: cfg.sourceLocale,\n locales: cfg.locales,\n // with include: [] nothing is scanned, so an empty origins map is off, not \"none found\"\n scanning: cfg.include.length > 0,\n catalogs,\n origins: await collectOrigins(cfg, registry),\n status: status(cfg, catalogs, registry, live, sidecar.fingerprints),\n check: relativize(cfg, check(cfg, catalogs, registry, sidecar.fingerprints)),\n drafts: live,\n triage: perLocale,\n problems,\n };\n}\n\nexport function health(cfg: ResolvedConfig): Promise<DoctorResult> {\n return doctor(cfg);\n}\n\nexport interface WriteMessageResult {\n locale: string;\n key: string;\n clearedDraft: boolean;\n}\n\n// A human wrote it, so it is reviewed and stamped for the text that ships, as import does.\nexport async function writeMessage(\n cfg: ResolvedConfig,\n locale: string,\n key: string,\n text: string,\n): Promise<WriteMessageResult> {\n if (locale === cfg.sourceLocale) {\n throw badRequest('the source text lives in your code, Studio never edits it');\n }\n if (!cfg.locales.includes(locale)) {\n throw badRequest(`${locale} is not one of this project's locales`);\n }\n // the review state is written after the catalog: an unreadable one stops the save before it\n const before = loadState(cfg);\n const catalogs = { [cfg.sourceLocale]: readCatalog(cfg, cfg.sourceLocale) };\n const registry = await extractProject(cfg);\n // checked against the text that ships, the one check reads every translation against\n const source = shippedCatalogs(cfg, catalogs, registry, { newKeys: false })[cfg.sourceLocale]!;\n // hasOwn, not `in`: `in` walks the prototype, so toString and __proto__ would pass this guard\n if (!Object.hasOwn(source, key)) {\n throw badRequest(`\"${key}\" is not in the source catalog, so Studio will not create it`);\n }\n // '' is untranslated everywhere else, so whitespace is that and not a translation to validate\n const value = text.trim() ? text : '';\n // the same two calls check() makes: validatePair alone misses a plural block with no other case\n const issues = value\n ? [...validateMessage(value, locale), ...validatePair(source[key] as string, value)]\n : [];\n const errors = issues.filter((issue) => issue.severity === 'error');\n if (errors.length) {\n throw badRequest(`${errors.map((issue) => issue.message).join('; ')}`);\n }\n\n const catalog = { ...readCatalog(cfg, locale), [key]: value };\n writeCatalog(cfg, locale, catalog);\n\n const clearedDraft = (before.drafts[locale] ?? []).includes(key);\n recordTranslations(cfg, source, [{ locale, entries: [{ key, text: value }], draft: false }]);\n return { locale, key, clearedDraft };\n}\n\nexport interface ApproveResult {\n locale: string;\n approved: number;\n keys: string[];\n}\n\n// The sidecar is re-read here: saveDrafts writes every locale, so a stale copy drops another's.\nexport function approve(cfg: ResolvedConfig, locale: string, keys?: string[]): ApproveResult {\n if (!cfg.locales.includes(locale)) {\n throw badRequest(`${locale} is not one of this project's locales`);\n }\n const drafts = loadDrafts(cfg);\n const had = drafts[locale] ?? [];\n // the keys it really cleared, because undo has to put back exactly those and nothing else\n const cleared = keys ? had.filter((key) => keys.includes(key)) : [...had];\n clearDrafts(drafts, locale, keys);\n saveDrafts(cfg, drafts);\n return { locale, approved: cleared.length, keys: cleared };\n}\n\n// Undo is not a nicety here: the flag records who wrote the text, so restoring it restores a fact.\nexport function unapprove(cfg: ResolvedConfig, locale: string, keys: string[]): ApproveResult {\n if (!cfg.locales.includes(locale)) {\n throw badRequest(`${locale} is not one of this project's locales`);\n }\n const drafts = loadDrafts(cfg);\n markDrafts(drafts, locale, keys);\n saveDrafts(cfg, drafts);\n return { locale, approved: keys.length, keys };\n}\n","import { badRequest, HttpError } from './http';\n\nexport type JobState = 'running' | 'done' | 'failed';\n\nexport interface Job {\n id: string;\n kind: 'translate';\n state: JobState;\n total: number;\n done: number;\n locale?: string;\n message?: string;\n result?: unknown;\n written?: number;\n kept?: number;\n}\n\n// One at a time, because two runs over the same catalogs is a race over the same files.\nlet current: Job | null = null;\nlet counter = 0;\n\nexport function running(): Job | null {\n return current?.state === 'running' ? current : null;\n}\n\nexport function read(id: string): Job {\n if (!current || current.id !== id) throw new HttpError(404, `no job called \"${id}\"`);\n return current;\n}\n\nexport function start(kind: Job['kind'], total: number): Job {\n if (running()) throw badRequest('a job is already running, wait for it or read its progress');\n counter += 1;\n current = { id: `${kind}-${counter}`, kind, state: 'running', total, done: 0 };\n return current;\n}\n\n// A batch that failed still finishes: the bar reports work attempted, and message says what broke.\nexport function advance(id: string, keys: number, locale: string, error?: string): void {\n if (current?.id !== id) return;\n current.done = Math.min(current.done + keys, current.total);\n current.locale = locale;\n if (error) current.message = error;\n}\n\n// A run that writes part of its work and then fails is a success for what it wrote, so both land.\nexport function finish(id: string, state: 'done' | 'failed', patch: Partial<Job> = {}): void {\n if (current?.id === id) Object.assign(current, patch, { state });\n}\n","import {\n collectOrigins,\n counted,\n extractProject,\n loadCatalogs,\n loadState,\n mergeTranslations,\n recordTranslations,\n resolveProvider,\n shippedCatalogs,\n syncProject,\n translateCatalogs,\n} from '@verbaly/compiler';\nimport type { ResolvedConfig, TranslationWrite } from '@verbaly/compiler';\nimport { badRequest } from './http';\nimport { advance, finish, start, type Job } from './jobs';\n\nexport interface ExtractResult {\n added: Record<string, string[]>;\n // only the source locale counts as new text: a target gains a blank for every gap it had\n found: number;\n // source texts the code wrote over, a hand edit in the catalog among them\n replaced: number;\n messages: number;\n // the sidecar could not be read or written: the catalogs were, its drafts and stamps were not\n stateProblem?: string;\n}\n\n// Local, fast and free, so it answers in the same request instead of becoming a job.\nexport async function runExtract(cfg: ResolvedConfig): Promise<ExtractResult> {\n if (!cfg.include.length) {\n throw badRequest('source scanning is off (include: []), so there is nothing to extract');\n }\n // the same path as the CLI: catalogs, types and the review state move together\n const result = await syncProject(cfg);\n return {\n added: result.added,\n found: result.added[cfg.sourceLocale]?.length ?? 0,\n replaced: result.replaced.length,\n messages: result.registry.messages().size,\n ...(result.stateProblem && { stateProblem: result.stateProblem }),\n };\n}\n\nexport interface Plan {\n pending: Record<string, string[]>;\n total: number;\n}\n\n// translate is the one command that spends money and minutes, so the panel always shows the bill.\nexport async function planTranslate(cfg: ResolvedConfig, locales?: string[]): Promise<Plan> {\n const provider = await resolveProvider(cfg);\n const catalogs = loadCatalogs(cfg);\n const result = await translateCatalogs(cfg, catalogs, provider, { locales, dryRun: true });\n const total = Object.values(result.pending).reduce((n, keys) => n + keys.length, 0);\n return { pending: result.pending, total };\n}\n\nexport async function startTranslate(cfg: ResolvedConfig, locales?: string[]): Promise<Job> {\n // a machine translation that lost its draft flag passes as reviewed: read the state first\n loadState(cfg);\n const provider = await resolveProvider(cfg);\n // the job carries its own denominator, so progress reads the same whether or not you asked first\n const plan = await planTranslate(cfg, locales);\n const job = start('translate', plan.total);\n const id = job.id;\n\n // the run outlives this request on purpose: the client asks the job how it is going\n void (async () => {\n try {\n const registry = await extractProject(cfg);\n // the provider reads the text that ships, which is the text its answer has to match\n const catalogs = shippedCatalogs(cfg, loadCatalogs(cfg), registry, { newKeys: false });\n const result = await translateCatalogs(cfg, catalogs, provider, {\n locales,\n batchSize: cfg.translate.batchSize,\n concurrency: cfg.translate.concurrency,\n retries: cfg.translate.retries,\n origins: await collectOrigins(cfg, registry),\n onProgress: (p) => advance(id, p.keys, p.locale, p.error),\n });\n // what a machine wrote stays a draft: the panel does not get to change that rule\n const writes: TranslationWrite[] = [];\n let written = 0;\n let kept = 0;\n for (const [locale, keys] of Object.entries(result.translated)) {\n const catalog = catalogs[locale] ?? {};\n const landed = mergeTranslations(cfg, locale, catalog, keys);\n writes.push({\n locale,\n draft: true,\n entries: landed.map((key) => ({ key, text: catalog[key]! })),\n });\n written += landed.length;\n kept += keys.length - landed.length;\n }\n recordTranslations(cfg, catalogs[cfg.sourceLocale] ?? {}, writes);\n // on a failure the panel shows only this line, so it says what landed and what did not\n const failed = result.failed[0];\n finish(id, failed ? 'failed' : 'done', {\n result,\n written,\n kept,\n message: failed\n ? `${counted(written, 'message')} translated, but ${counted(result.failed.length, 'batch', 'batches')} did not answer (${failed.error}): run it again for the rest`\n : undefined,\n });\n } catch (error) {\n finish(id, 'failed', { message: error instanceof Error ? error.message : String(error) });\n }\n })();\n\n return job;\n}\n","import { execFile, spawn } from 'node:child_process';\nimport { promisify } from 'node:util';\nimport { join, relative } from 'node:path';\nimport { parseCatalog, type Catalog, type ResolvedConfig } from '@verbaly/compiler';\n\nconst run = promisify(execFile);\n\nexport interface Commit {\n sha: string;\n author: string;\n date: string;\n subject: string;\n}\n\n// one field separator that no subject can contain, written as the escape and never as the byte\nconst FIELD = '\\u001f';\nconst FORMAT = `--format=%H${FIELD}%an${FIELD}%aI${FIELD}%s`;\n\n// the full sha is what git resolves without ambiguity, and seven characters is what a card shows\nexport function parseFullLog(stdout: string): Commit[] {\n const commits: Commit[] = [];\n for (const line of stdout.split('\\n')) {\n const [sha, author, date, ...rest] = line.split(FIELD);\n // a subject holds anything but a newline, so it is the last field and is rejoined\n if (!sha || !author || !date || !rest.length) continue;\n commits.push({ sha, author, date, subject: rest.join(FIELD) });\n }\n return commits;\n}\n\n// split out so the shape of a log line is tested without a repository and without writing one\nexport function parseLog(stdout: string): Commit[] {\n return parseFullLog(stdout).map((commit) => ({ ...commit, sha: commit.sha.slice(0, 7) }));\n}\n\n// git is asked, never assumed: no repository and no git both mean there is no history to show\nexport async function catalogHistory(cfg: ResolvedConfig, limit = 5): Promise<Commit[]> {\n const dir = relative(cfg.root, cfg.dir).replaceAll('\\\\', '/') || '.';\n try {\n const { stdout } = await run('git', ['log', `-n${limit}`, FORMAT, '--', dir], {\n cwd: cfg.root,\n timeout: 3000,\n windowsHide: true,\n });\n return parseLog(stdout);\n } catch {\n return [];\n }\n}\n\nexport interface Revision {\n commit: Commit;\n now: Catalog | undefined;\n before: Catalog | undefined;\n}\n\n// blame measured 26% wrong on verbaly-web: a sibling's comma rewrites a line, a value does not\nexport function attribute(\n revisions: Revision[],\n owners = new Map<string, Commit>(),\n): Map<string, Commit> {\n for (const { commit, now, before } of revisions) {\n if (!now || !before) continue;\n for (const key of new Set([...Object.keys(now), ...Object.keys(before)])) {\n if (!owners.has(key) && now[key] !== before[key]) owners.set(key, commit);\n }\n }\n return owners;\n}\n\n// a key can move in more than one catalog, so the answer is the newest of the per-file answers\nexport function mergeOwners(maps: Map<string, Commit>[]): Map<string, Commit> {\n const merged = new Map<string, Commit>();\n for (const map of maps) {\n for (const [key, commit] of map) {\n const held = merged.get(key);\n if (!held || Date.parse(commit.date) > Date.parse(held.date)) merged.set(key, commit);\n }\n }\n return merged;\n}\n\nconst BLOB = /^[0-9a-f]{40} \\w+ (\\d+)$/;\n\n// git cat-file --batch answers every revision in one process: a header line, then that many bytes\nfunction readBlobs(\n root: string,\n requests: string[],\n take: (index: number, body: string | null, blob: string) => void,\n): Promise<boolean> {\n return new Promise((resolve) => {\n const child = spawn('git', ['cat-file', '--batch'], { cwd: root, windowsHide: true });\n // a killed batch leaves a half map, and a half map answers \"never committed\" for the rest\n const stop = setTimeout(() => {\n ok = false;\n child.kill();\n }, 20000);\n let pending: Buffer = Buffer.alloc(0);\n let index = 0;\n let ok = true;\n const fail = () => {\n ok = false;\n };\n child.on('error', fail);\n child.stdin.on('error', fail);\n child.stdout.on('data', (chunk: Buffer) => {\n pending = pending.length ? Buffer.concat([pending, chunk]) : chunk;\n for (;;) {\n const end = pending.indexOf(10);\n if (end < 0) break;\n const header = pending.toString('utf8', 0, end);\n const blob = BLOB.exec(header);\n // anything but a blob header is a revision git could not resolve, which is an answer too\n if (!blob) {\n take(index++, null, '');\n pending = pending.subarray(end + 1);\n continue;\n }\n const size = Number(blob[1]);\n if (pending.length < end + size + 2) break;\n take(index++, pending.toString('utf8', end + 1, end + 1 + size), header.slice(0, 40));\n pending = pending.subarray(end + size + 2);\n }\n });\n child.on('close', () => {\n clearTimeout(stop);\n resolve(ok);\n });\n child.stdin.end(requests.join('\\n') + '\\n');\n });\n}\n\nexport interface KeyHistory {\n owners: Map<string, Commit>;\n committed: Set<string>;\n ok: boolean;\n}\n\n// the window a local tool is willing to read: past this, the answer is \"older than what I read\"\nconst WINDOW = 200;\n\nasync function walkFile(root: string, file: string, limit: number) {\n const { stdout } = await run('git', ['log', `-n${limit}`, FORMAT, '--', file], {\n cwd: root,\n timeout: 5000,\n windowsHide: true,\n maxBuffer: 8_000_000,\n });\n const commits = parseFullLog(stdout);\n if (!commits.length) return { owners: new Map<string, Commit>(), head: [] as string[] };\n\n const owners = new Map<string, Commit>();\n let head: string[] = [];\n let waiting: Catalog | undefined;\n // only two flat catalogs are ever live, and the pair shares a blob, so one entry caches it\n let last: { blob: string; flat: Catalog | undefined } | null = null;\n const parse = (body: string | null, blob: string): Catalog | undefined => {\n if (body === null) return {};\n if (last && last.blob === blob) return last.flat;\n const flat = parseCatalog(body);\n last = { blob, flat };\n return flat;\n };\n\n const whole = await readBlobs(\n root,\n commits.flatMap((commit) => [`${commit.sha}:${file}`, `${commit.sha}^:${file}`]),\n (index, body, blob) => {\n const flat = parse(body, blob);\n if (index % 2 === 0) {\n waiting = flat;\n if (index === 0 && flat) head = Object.keys(flat);\n return;\n }\n const commit = commits[(index - 1) / 2];\n if (commit) attribute([{ commit, now: waiting, before: flat }], owners);\n waiting = undefined;\n },\n );\n if (!whole) throw new Error('[verbaly] git could not read every revision of this catalog');\n return { owners, head };\n}\n\n// The whole map at once, because a per-key query costs a git process and every row has a menu.\nexport async function catalogKeyHistory(\n cfg: ResolvedConfig,\n limit = WINDOW,\n): Promise<KeyHistory> {\n const maps: Map<string, Commit>[] = [];\n const committed = new Set<string>();\n let ok = false;\n for (const locale of cfg.locales) {\n const file = relative(cfg.root, join(cfg.dir, `${locale}.json`)).replaceAll('\\\\', '/');\n try {\n const walked = await walkFile(cfg.root, file, limit);\n maps.push(walked.owners);\n for (const key of walked.head) committed.add(key);\n ok = true;\n } catch {\n // one unreadable locale is not the end of the walk: the others still answer for their keys\n }\n }\n return { owners: mergeOwners(maps), committed, ok };\n}\n\nexport interface KeyCommit {\n key: string;\n commit: Commit | null;\n reason?: 'nogit' | 'uncommitted' | 'older';\n}\n\nconst walked = new Map<string, { head: string; history: KeyHistory }>();\n\nasync function headSha(root: string): Promise<string> {\n try {\n const { stdout } = await run('git', ['rev-parse', 'HEAD'], {\n cwd: root,\n timeout: 3000,\n windowsHide: true,\n });\n return stdout.trim();\n } catch {\n return '';\n }\n}\n\n// HEAD moving is the only thing that can change an answer, so it is what invalidates the walk\nasync function keyHistory(cfg: ResolvedConfig): Promise<KeyHistory> {\n const head = await headSha(cfg.root);\n const held = walked.get(cfg.root);\n if (held && held.head === head) return held.history;\n const history = await catalogKeyHistory(cfg);\n walked.set(cfg.root, { head, history });\n return history;\n}\n\nexport async function keyCommit(cfg: ResolvedConfig, key: string): Promise<KeyCommit> {\n const history = await keyHistory(cfg);\n if (!history.ok) return { key, commit: null, reason: 'nogit' };\n const commit = history.owners.get(key);\n // seven characters is what a person reads and pastes, and the walk needed the full one\n if (commit) return { key, commit: { ...commit, sha: commit.sha.slice(0, 7) } };\n return { key, commit: null, reason: history.committed.has(key) ? 'older' : 'uncommitted' };\n}\n","import { existsSync, readFileSync, statSync } from 'node:fs';\nimport { join, normalize, resolve } from 'node:path';\nimport { fileURLToPath } from 'node:url';\n\n// the built panel sits beside the compiled server, so it is found relative to this file, never cwd\nconst UI = resolve(fileURLToPath(new URL('.', import.meta.url)), 'ui');\n\nconst TYPES: Record<string, string> = {\n '.html': 'text/html; charset=utf-8',\n '.js': 'text/javascript; charset=utf-8',\n '.css': 'text/css; charset=utf-8',\n '.svg': 'image/svg+xml',\n '.woff2': 'font/woff2',\n '.json': 'application/json; charset=utf-8',\n '.ico': 'image/x-icon',\n};\n\nexport interface Asset {\n body: Buffer;\n type: string;\n}\n\nexport const uiBuilt = (): boolean => existsSync(join(UI, 'index.html'));\n\n// A path is resolved and then checked to still be inside UI: ../ and an absolute one both land out.\nexport function readAsset(pathname: string): Asset | undefined {\n const wanted = pathname === '/' ? '/index.html' : pathname;\n const target = resolve(join(UI, normalize(wanted)));\n if (target !== UI && !target.startsWith(UI + (process.platform === 'win32' ? '\\\\' : '/'))) {\n return undefined;\n }\n if (!existsSync(target) || !statSync(target).isFile()) return undefined;\n const dot = target.lastIndexOf('.');\n return {\n body: readFileSync(target),\n type: TYPES[target.slice(dot).toLowerCase()] ?? 'application/octet-stream',\n };\n}\n","import { randomBytes, timingSafeEqual } from 'node:crypto';\n\nexport interface GuardOptions {\n port: number;\n token: string;\n}\n\n// A local server that writes files is reachable from any page the browser has open.\nexport function newToken(): string {\n return randomBytes(9).toString('base64url');\n}\n\n// Only the two names that resolve to this machine. A DNS rebind arrives with a foreign Host.\nexport function hostAllowed(host: string | undefined, port: number): boolean {\n // a browser omits the port when it is the scheme default, so :80 would 403 every real request\n const names = port === 80 ? ['127.0.0.1', 'localhost'] : [];\n return [`127.0.0.1:${port}`, `localhost:${port}`, ...names].includes(host ?? '');\n}\n\n// No Origin at all is a same-origin navigation or a curl, both fine; a foreign one never is.\nexport function originAllowed(origin: string | undefined, port: number): boolean {\n if (origin === undefined) return true;\n return origin === `http://127.0.0.1:${port}` || origin === `http://localhost:${port}`;\n}\n\n// Compared in constant time so the token cannot be guessed one character at a time.\nexport function tokenMatches(given: string | null, expected: string): boolean {\n if (given === null) return false;\n const a = Buffer.from(given);\n const b = Buffer.from(expected);\n return a.length === b.length && timingSafeEqual(a, b);\n}\n\nexport interface GuardVerdict {\n ok: boolean;\n status: number;\n reason: string;\n}\n\nconst PASS: GuardVerdict = { ok: true, status: 200, reason: '' };\n\nexport function guardRequest(\n headers: { host?: string; origin?: string },\n url: URL,\n { port, token }: GuardOptions,\n): GuardVerdict {\n if (!hostAllowed(headers.host, port)) {\n return { ok: false, status: 403, reason: '[verbaly] unexpected Host header' };\n }\n if (!originAllowed(headers.origin, port)) {\n return { ok: false, status: 403, reason: '[verbaly] cross-origin request' };\n }\n if (!tokenMatches(url.searchParams.get('t'), token)) {\n return { ok: false, status: 401, reason: '[verbaly] missing or wrong token, open the printed url' };\n }\n return PASS;\n}\n","import { createServer, type IncomingMessage, type Server, type ServerResponse } from 'node:http';\nimport type { AddressInfo } from 'node:net';\nimport type { ResolvedConfig } from '@verbaly/compiler';\nimport { approve, buildState, health, unapprove, writeMessage } from './api';\nimport { planTranslate, runExtract, startTranslate } from './actions';\nimport { read as readJob, running } from './jobs';\nimport { catalogHistory, keyCommit } from './history';\nimport { readAsset, uiBuilt } from './assets';\nimport { guardRequest, newToken } from './guard';\nimport { HttpError, badRequest, scrub } from './http';\n\nexport interface StudioServer {\n server: Server;\n port: number;\n token: string;\n url: string;\n close: () => Promise<void>;\n}\n\nexport interface StartOptions {\n port?: number;\n token?: string;\n}\n\nconst DEFAULT_PORT = 4747;\nconst MAX_BODY = 1_000_000;\n\nfunction send(res: ServerResponse, status: number, body: unknown): void {\n res.writeHead(status, {\n 'content-type': 'application/json; charset=utf-8',\n 'cache-control': 'no-store',\n });\n res.end(JSON.stringify(body));\n}\n\nasync function readJson(req: IncomingMessage): Promise<Record<string, unknown>> {\n const chunks: Buffer[] = [];\n let size = 0;\n for await (const chunk of req) {\n size += (chunk as Buffer).length;\n if (size > MAX_BODY) throw new HttpError(413, 'request body too large');\n chunks.push(chunk as Buffer);\n }\n if (!chunks.length) return {};\n let parsed: unknown;\n try {\n parsed = JSON.parse(Buffer.concat(chunks).toString('utf8'));\n } catch {\n throw badRequest('body is not valid JSON');\n }\n // null and an array both pass typeof 'object', and reading .text off them is a 500\n if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {\n throw badRequest('body must be a JSON object');\n }\n return parsed as Record<string, unknown>;\n}\n\nfunction segment(raw: string | undefined, what: string): string {\n let value: string;\n try {\n value = decodeURIComponent(raw ?? '');\n } catch {\n throw badRequest(`the ${what} in the url is not valid percent-encoding`);\n }\n if (!value) throw badRequest(`the url is missing a ${what}`);\n return value;\n}\n\n// One page, JSON on every route: a framework would only add a dependency to a local tool.\nexport function createStudioApp(cfg: ResolvedConfig, port: number, token: string) {\n return async function handle(req: IncomingMessage, res: ServerResponse): Promise<void> {\n // The url is built inside the try: `GET //` throws, and out here that would kill the process.\n try {\n let url: URL;\n try {\n url = new URL(req.url ?? '/', `http://127.0.0.1:${port}`);\n } catch {\n throw badRequest('the request target is not a valid url');\n }\n\n const api = url.pathname.startsWith('/api/');\n const verdict = guardRequest({ host: req.headers.host, origin: req.headers.origin }, url, {\n port,\n token,\n });\n // the panel files are the published npm package: only the project data needs a token\n if (!verdict.ok && (api || verdict.status !== 401)) {\n send(res, verdict.status, { error: verdict.reason });\n return;\n }\n\n if (!api && req.method === 'GET') {\n const asset = readAsset(url.pathname);\n if (asset) {\n res.writeHead(200, { 'content-type': asset.type, 'cache-control': 'no-store' });\n res.end(asset.body);\n return;\n }\n if (url.pathname === '/' && !uiBuilt()) {\n throw new HttpError(500, 'the panel is not in this install, so only /api/* answers here');\n }\n }\n\n if (req.method === 'GET' && url.pathname === '/api/state') {\n send(res, 200, await buildState(cfg));\n return;\n }\n if (req.method === 'GET' && url.pathname === '/api/history') {\n send(res, 200, { commits: await catalogHistory(cfg) });\n return;\n }\n if (req.method === 'GET' && url.pathname.startsWith('/api/commit/')) {\n const key = segment(url.pathname.slice('/api/commit/'.length), 'key');\n send(res, 200, await keyCommit(cfg, key));\n return;\n }\n if (req.method === 'GET' && url.pathname === '/api/health') {\n send(res, 200, await health(cfg));\n return;\n }\n if (req.method === 'PUT' && url.pathname.startsWith('/api/message/')) {\n const rest = url.pathname.slice('/api/message/'.length).split('/');\n const locale = segment(rest[0], 'locale');\n const key = segment(rest.slice(1).join('/'), 'key');\n const body = await readJson(req);\n if (typeof body.text !== 'string') throw badRequest('body needs a \"text\" string');\n send(res, 200, await writeMessage(cfg, locale, key, body.text));\n return;\n }\n if (req.method === 'POST' && url.pathname === '/api/approve') {\n const body = await readJson(req);\n if (typeof body.locale !== 'string') throw badRequest('body needs a \"locale\" string');\n const keys = Array.isArray(body.keys)\n ? body.keys.filter((k): k is string => typeof k === 'string')\n : undefined;\n if (body.undo === true) {\n if (!keys?.length) throw badRequest('undoing needs the \"keys\" it should put back');\n send(res, 200, unapprove(cfg, body.locale, keys));\n return;\n }\n send(res, 200, approve(cfg, body.locale, keys));\n return;\n }\n if (req.method === 'POST' && url.pathname === '/api/extract') {\n send(res, 200, await runExtract(cfg));\n return;\n }\n if (req.method === 'GET' && url.pathname === '/api/translate') {\n const locales = url.searchParams.getAll('locale');\n send(res, 200, await planTranslate(cfg, locales.length ? locales : undefined));\n return;\n }\n if (req.method === 'POST' && url.pathname === '/api/translate') {\n const body = await readJson(req);\n const locales = Array.isArray(body.locales)\n ? body.locales.filter((l): l is string => typeof l === 'string')\n : undefined;\n send(res, 200, await startTranslate(cfg, locales?.length ? locales : undefined));\n return;\n }\n // a reload loses the id, so the panel asks what is running rather than losing the bar\n if (req.method === 'GET' && url.pathname === '/api/job') {\n send(res, 200, running());\n return;\n }\n if (req.method === 'GET' && url.pathname.startsWith('/api/job/')) {\n send(res, 200, readJob(segment(url.pathname.slice('/api/job/'.length), 'job id')));\n return;\n }\n\n send(res, 404, { error: `[verbaly] no route for ${req.method} ${url.pathname}` });\n } catch (error) {\n const status = error instanceof HttpError ? error.status : 500;\n const message = error instanceof Error ? error.message : String(error);\n send(res, status, { error: scrub(message, cfg.root) });\n }\n };\n}\n\n// A busy port climbs instead of failing: this is a local tool, not a service on a fixed address.\nfunction listen(server: Server, port: number, attemptsLeft: number): Promise<number> {\n return new Promise((resolve, reject) => {\n const onError = (error: NodeJS.ErrnoException) => {\n if (error.code === 'EADDRINUSE' && attemptsLeft > 0) {\n server.removeListener('error', onError);\n resolve(listen(server, port + 1, attemptsLeft - 1));\n return;\n }\n reject(error);\n };\n server.once('error', onError);\n server.listen(port, '127.0.0.1', () => {\n server.removeListener('error', onError);\n // port 0 asks the system to pick, so the bound address is the only place the answer is\n const bound = server.address() as AddressInfo | string | null;\n resolve(typeof bound === 'object' && bound !== null ? bound.port : port);\n });\n });\n}\n\nexport async function startStudio(\n cfg: ResolvedConfig,\n options: StartOptions = {},\n): Promise<StudioServer> {\n const token = options.token ?? newToken();\n const server = createServer();\n // The handler is attached before listen so no request can arrive without one.\n let port = options.port ?? DEFAULT_PORT;\n server.on('request', (req, res) => void createStudioApp(cfg, port, token)(req, res));\n port = await listen(server, port, 20);\n\n return {\n server,\n port,\n token,\n url: `http://127.0.0.1:${port}/?t=${token}`,\n close: () =>\n new Promise<void>((resolve, reject) =>\n server.close((error) => (error ? reject(error) : resolve())),\n ),\n };\n}\n","#!/usr/bin/env node\nimport { parseArgs } from 'node:util';\nimport { counted, formatCliError, loadConfig } from '@verbaly/compiler';\nimport { buildState } from './api';\nimport { startStudio } from './server';\n\ntry {\n const { values } = parseArgs({\n options: {\n root: { type: 'string' },\n port: { type: 'string' },\n json: { type: 'boolean' },\n },\n });\n const port = values.port === undefined ? undefined : Number(values.port);\n if (port !== undefined && !Number.isInteger(port)) {\n throw new Error(`[verbaly] --port needs a number, got \"${values.port}\"`);\n }\n const cfg = await loadConfig(values.root ?? process.cwd());\n\n // --json is the state the interface reads, on stdout, before any interface exists.\n if (values.json) {\n console.log(JSON.stringify(await buildState(cfg), null, 2));\n } else {\n const studio = await startStudio(cfg, { port });\n const state = await buildState(cfg);\n const missing = state.status.locales.reduce((n, l) => n + (l.total - l.translated), 0);\n const drafts = Object.values(state.drafts).reduce((n, keys) => n + keys.length, 0);\n console.log(`\\n Verbaly Studio ${studio.url}`);\n console.log(` Project ${cfg.root}`);\n console.log(\n ` Catalog ${counted(state.status.messages, 'message')} · ` +\n `${missing} untranslated · ${counted(drafts, 'draft')}`,\n );\n for (const problem of state.problems) console.log(` ! ${problem.scope}: ${problem.message}`);\n console.log('\\n Ctrl+C to stop\\n');\n }\n} catch (error) {\n console.error(formatCliError(error));\n process.exitCode = 1;\n}\n"],"mappings":"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"}
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { createServer } from "node:http";
2
2
  import { existsSync, readFileSync, statSync } from "node:fs";
3
3
  import { join, normalize, relative, resolve } from "node:path";
4
- import { DRAFTS_FILE, check, clearDrafts, collectOrigins, counted, doctor, effectiveDrafts, extractProject, loadCatalogs, loadDrafts, markDrafts, mergeTranslations, parseCatalog, readCatalog, resolveProvider, saveDrafts, status, syncCatalogs, targetLocales, translateCatalogs, validateMessage, validatePair, writeCatalog, writeDts } from "@verbaly/compiler";
4
+ import { STATE_FILE, check, clearDrafts, collectOrigins, counted, doctor, effectiveDrafts, extractProject, loadCatalogs, loadDrafts, loadState, markDrafts, mergeTranslations, parseCatalog, readCatalog, readState, recordTranslations, resolveProvider, saveDrafts, shippedCatalogs, status, syncProject, targetLocales, translateCatalogs, validateMessage, validatePair, writeCatalog } from "@verbaly/compiler";
5
5
  import { execFile, spawn } from "node:child_process";
6
6
  import { promisify } from "node:util";
7
7
  import { fileURLToPath } from "node:url";
@@ -152,23 +152,6 @@ function readAll(cfg) {
152
152
  problems
153
153
  };
154
154
  }
155
- function readDrafts(cfg) {
156
- try {
157
- return {
158
- drafts: loadDrafts(cfg),
159
- problems: []
160
- };
161
- } catch (error) {
162
- const message = error instanceof Error ? error.message : String(error);
163
- return {
164
- drafts: {},
165
- problems: [{
166
- scope: DRAFTS_FILE,
167
- message
168
- }]
169
- };
170
- }
171
- }
172
155
  function relativize(cfg, result) {
173
156
  const rel = (file) => relative(cfg.root, file).replaceAll("\\", "/");
174
157
  return {
@@ -180,14 +163,31 @@ function relativize(cfg, result) {
180
163
  extra: result.extra.map((entry) => ({
181
164
  ...entry,
182
165
  files: entry.files.map(rel)
166
+ })),
167
+ collisions: result.collisions.map((entry) => ({
168
+ ...entry,
169
+ sites: entry.sites.map((at) => ({
170
+ ...at,
171
+ file: rel(at.file)
172
+ }))
173
+ })),
174
+ divergent: result.divergent.map((entry) => ({
175
+ ...entry,
176
+ code: {
177
+ ...entry.code,
178
+ file: rel(entry.code.file)
179
+ }
183
180
  }))
184
181
  };
185
182
  }
186
183
  async function buildState(cfg) {
187
184
  const { catalogs, problems } = readAll(cfg);
188
185
  const registry = await extractProject(cfg);
189
- const sidecar = readDrafts(cfg);
190
- problems.push(...sidecar.problems);
186
+ const { state: sidecar, problem } = readState(cfg);
187
+ if (problem) problems.push({
188
+ scope: STATE_FILE,
189
+ message: problem
190
+ });
191
191
  const live = effectiveDrafts(sidecar.drafts, catalogs);
192
192
  const source = catalogs[cfg.sourceLocale] ?? {};
193
193
  const targets = targetLocales(cfg);
@@ -212,8 +212,8 @@ async function buildState(cfg) {
212
212
  scanning: cfg.include.length > 0,
213
213
  catalogs,
214
214
  origins: await collectOrigins(cfg, registry),
215
- status: status(cfg, catalogs, registry, live),
216
- check: relativize(cfg, check(cfg, catalogs, registry)),
215
+ status: status(cfg, catalogs, registry, live, sidecar.fingerprints),
216
+ check: relativize(cfg, check(cfg, catalogs, registry, sidecar.fingerprints)),
217
217
  drafts: live,
218
218
  triage: perLocale,
219
219
  problems
@@ -222,10 +222,13 @@ async function buildState(cfg) {
222
222
  function health(cfg) {
223
223
  return doctor(cfg);
224
224
  }
225
- function writeMessage(cfg, locale, key, text) {
225
+ async function writeMessage(cfg, locale, key, text) {
226
226
  if (locale === cfg.sourceLocale) throw badRequest("the source text lives in your code, Studio never edits it");
227
227
  if (!cfg.locales.includes(locale)) throw badRequest(`${locale} is not one of this project's locales`);
228
- const source = readCatalog(cfg, cfg.sourceLocale);
228
+ const before = loadState(cfg);
229
+ const catalogs = { [cfg.sourceLocale]: readCatalog(cfg, cfg.sourceLocale) };
230
+ const registry = await extractProject(cfg);
231
+ const source = shippedCatalogs(cfg, catalogs, registry, { newKeys: false })[cfg.sourceLocale];
229
232
  if (!Object.hasOwn(source, key)) throw badRequest(`"${key}" is not in the source catalog, so Studio will not create it`);
230
233
  const value = text.trim() ? text : "";
231
234
  const errors = (value ? [...validateMessage(value, locale), ...validatePair(source[key], value)] : []).filter((issue) => issue.severity === "error");
@@ -235,10 +238,15 @@ function writeMessage(cfg, locale, key, text) {
235
238
  [key]: value
236
239
  };
237
240
  writeCatalog(cfg, locale, catalog);
238
- const drafts = loadDrafts(cfg);
239
- const clearedDraft = (drafts[locale] ?? []).includes(key);
240
- clearDrafts(drafts, locale, [key]);
241
- saveDrafts(cfg, drafts);
241
+ const clearedDraft = (before.drafts[locale] ?? []).includes(key);
242
+ recordTranslations(cfg, source, [{
243
+ locale,
244
+ entries: [{
245
+ key,
246
+ text: value
247
+ }],
248
+ draft: false
249
+ }]);
242
250
  return {
243
251
  locale,
244
252
  key,
@@ -305,16 +313,13 @@ function finish(id, state, patch = {}) {
305
313
  //#region src/actions.ts
306
314
  async function runExtract(cfg) {
307
315
  if (!cfg.include.length) throw badRequest("source scanning is off (include: []), so there is nothing to extract");
308
- const registry = await extractProject(cfg);
309
- const catalogs = loadCatalogs(cfg);
310
- const result = syncCatalogs(cfg, catalogs, registry);
311
- for (const locale of cfg.locales) writeCatalog(cfg, locale, catalogs[locale] ?? {});
312
- writeDts(cfg, catalogs[cfg.sourceLocale] ?? {});
313
- const found = result.added[cfg.sourceLocale]?.length ?? 0;
316
+ const result = await syncProject(cfg);
314
317
  return {
315
318
  added: result.added,
316
- found,
317
- messages: registry.messages().size
319
+ found: result.added[cfg.sourceLocale]?.length ?? 0,
320
+ replaced: result.replaced.length,
321
+ messages: result.registry.messages().size,
322
+ ...result.stateProblem && { stateProblem: result.stateProblem }
318
323
  };
319
324
  }
320
325
  async function planTranslate(cfg, locales) {
@@ -331,30 +336,40 @@ async function planTranslate(cfg, locales) {
331
336
  };
332
337
  }
333
338
  async function startTranslate(cfg, locales) {
339
+ loadState(cfg);
334
340
  const provider = await resolveProvider(cfg);
335
341
  const job = start("translate", (await planTranslate(cfg, locales)).total);
336
342
  const id = job.id;
337
343
  (async () => {
338
344
  try {
339
- const catalogs = loadCatalogs(cfg);
345
+ const registry = await extractProject(cfg);
346
+ const catalogs = shippedCatalogs(cfg, loadCatalogs(cfg), registry, { newKeys: false });
340
347
  const result = await translateCatalogs(cfg, catalogs, provider, {
341
348
  locales,
342
349
  batchSize: cfg.translate.batchSize,
343
350
  concurrency: cfg.translate.concurrency,
344
351
  retries: cfg.translate.retries,
345
- origins: await collectOrigins(cfg),
352
+ origins: await collectOrigins(cfg, registry),
346
353
  onProgress: (p) => advance(id, p.keys, p.locale, p.error)
347
354
  });
348
- const drafts = loadDrafts(cfg);
355
+ const writes = [];
349
356
  let written = 0;
350
357
  let kept = 0;
351
358
  for (const [locale, keys] of Object.entries(result.translated)) {
352
- const landed = mergeTranslations(cfg, locale, catalogs[locale] ?? {}, keys);
353
- markDrafts(drafts, locale, landed);
359
+ const catalog = catalogs[locale] ?? {};
360
+ const landed = mergeTranslations(cfg, locale, catalog, keys);
361
+ writes.push({
362
+ locale,
363
+ draft: true,
364
+ entries: landed.map((key) => ({
365
+ key,
366
+ text: catalog[key]
367
+ }))
368
+ });
354
369
  written += landed.length;
355
370
  kept += keys.length - landed.length;
356
371
  }
357
- saveDrafts(cfg, drafts);
372
+ recordTranslations(cfg, catalogs[cfg.sourceLocale] ?? {}, writes);
358
373
  const failed = result.failed[0];
359
374
  finish(id, failed ? "failed" : "done", {
360
375
  result,
@@ -751,7 +766,7 @@ function createStudioApp(cfg, port, token) {
751
766
  const key = segment(rest.slice(1).join("/"), "key");
752
767
  const body = await readJson(req);
753
768
  if (typeof body.text !== "string") throw badRequest("body needs a \"text\" string");
754
- send(res, 200, writeMessage(cfg, locale, key, body.text));
769
+ send(res, 200, await writeMessage(cfg, locale, key, body.text));
755
770
  return;
756
771
  }
757
772
  if (req.method === "POST" && url.pathname === "/api/approve") {
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":["readJob"],"sources":["../src/http.ts","../src/triage.ts","../src/api.ts","../src/jobs.ts","../src/actions.ts","../src/history.ts","../src/assets.ts","../src/guard.ts","../src/server.ts"],"sourcesContent":["// A client mistake is not a server fault, so the status rides with the error, not a blanket 500.\nexport class HttpError extends Error {\n readonly status: number;\n\n constructor(status: number, message: string) {\n super(message.startsWith('[verbaly]') ? message : `[verbaly] ${message}`);\n this.status = status;\n this.name = 'HttpError';\n }\n}\n\nexport const badRequest = (message: string) => new HttpError(400, message);\n\n// The absolute path of someone's project is not the browser's business.\nexport function scrub(message: string, root: string): string {\n return root ? message.split(root).join('.') : message;\n}\n","import type { Catalog } from '@verbaly/compiler';\n\nexport type TriageSignal = 'divergent' | 'collision' | 'echo' | 'digits' | 'code' | 'url';\n\nexport interface TriageReason {\n signal: TriageSignal;\n text: string;\n}\n\nexport type TriageResult = Record<string, TriageReason[]>;\n\n// check() already caught the structure, so these only point at the few worth reading first.\nexport interface TriageInput {\n source: Catalog;\n target: Catalog;\n reviewed: ReviewedLocale[];\n}\n\n// the locale rides with the catalog because a reason that cannot name the language explains nothing\nexport interface ReviewedLocale {\n locale: string;\n catalog: Catalog;\n}\n\nconst DIGITS = /\\d[\\d.,]*/g;\nconst URLS = /https?:\\/\\/[^\\s<)\"']+/g;\nconst CODE = /<code>([^<]+)<\\/code>/g;\n\nfunction bag(text: string, re: RegExp, normalize?: (v: string) => string): string[] {\n const found = text.match(re) ?? [];\n return (normalize ? found.map(normalize) : found).sort();\n}\n\nfunction same(a: string[], b: string[]): boolean {\n return a.length === b.length && a.every((v, i) => v === b[i]);\n}\n\n// A number or a url at the end of a clause takes the punctuation with it, and that is not a signal.\nconst trimPunct = (v: string) => v.replace(/[.,;:!?)\\]]+$/, '');\n// A separator is idiomatic (3.09 is 3,09 in Spanish); canonizing keeps 1.5 apart from 15.\nconst bareDigits = (v: string) => trimPunct(v).replaceAll(/[.,]/g, '.');\n\nfunction push(out: TriageResult, key: string, reason: TriageReason): void {\n (out[key] ??= []).push(reason);\n}\n\n// the column is three short lines, so a reason quotes the evidence and never the whole message\nfunction clip(text: string, max = 42): string {\n return text.length > max ? `${text.slice(0, max - 1)}…` : text;\n}\n\nconst quote = (text: string, max?: number) => `\"${clip(text, max)}\"`;\n\n// what the source has and the translation does not, which is the half worth naming\nfunction lost(from: string[], to: string[]): string[] {\n const rest = [...to];\n const out: string[] = [];\n for (const item of from) {\n const at = rest.indexOf(item);\n if (at < 0) out.push(item);\n else rest.splice(at, 1);\n }\n return out;\n}\n\n// \"the numbers moved\" is a label; naming the one that moved is what saves the reader a diff\nfunction movedText(kind: string, from: string[], to: string[]): string {\n const gone = lost(from, to);\n const added = lost(to, from);\n if (gone.length && added.length) {\n return `the source ${kind} ${quote(gone[0]!)}, this one ${kind} ${quote(added[0]!)}`;\n }\n if (gone.length) return `the source ${kind} ${quote(gone[0]!)} and this one does not`;\n return `this one ${kind} ${quote(added[0]!)} and the source does not`;\n}\n\nexport function triage({ source, target, reviewed }: TriageInput): TriageResult {\n // null-prototype: a source text of \"__proto__\" or \"toString\" is a string, not a member of Object\n const out: TriageResult = Object.create(null);\n const keys = Object.keys(source);\n\n // One source text that two humans rendered two ways is ambiguous, so the machine had no chance.\n const bySource: Record<string, string[]> = Object.create(null);\n for (const key of keys) {\n const text = source[key]?.trim();\n if (text) (bySource[text] ??= []).push(key);\n }\n for (const [text, group] of Object.entries(bySource)) {\n if (group.length < 2) continue;\n\n // the first reviewed locale that disagreed with itself, and the two renderings it used\n let split: { locale: string; first: string; second: string } | undefined;\n for (const { locale, catalog } of reviewed) {\n const seen = [...new Set(group.map((k) => catalog[k]?.trim() ?? '').filter(Boolean))];\n if (seen.length > 1) {\n split = { locale, first: seen[0]!, second: seen[1]! };\n break;\n }\n }\n if (split) {\n for (const key of group) {\n push(out, key, {\n signal: 'divergent',\n text: `${split.locale} already says this two ways: ${quote(split.first, 32)} and ${quote(split.second, 32)}`,\n });\n }\n }\n\n // The same ambiguity seen from the result instead of from the history.\n const targets = new Set(group.map((key) => target[key]?.trim() ?? '').filter(Boolean));\n if (!split && targets.size > 1) {\n for (const key of group) {\n const mine = target[key]?.trim() ?? '';\n const other = group.find((k) => k !== key && (target[k]?.trim() ?? '') !== mine && target[k]?.trim());\n push(out, key, {\n signal: 'collision',\n text: other\n ? `${other} has the same source text and says ${quote(target[other]!.trim())}`\n : `${quote(text)} came back as two different translations`,\n });\n }\n }\n }\n\n for (const key of keys) {\n const src = source[key]?.trim() ?? '';\n const trg = target[key]?.trim() ?? '';\n if (!src || !trg) continue;\n\n // Only the locales with an opinion on this key vote: alone it fires on every do-not-translate.\n const voters = reviewed.filter(({ catalog }) => {\n const value = catalog[key]?.trim();\n return value !== undefined && value !== '';\n });\n if (trg === src && voters.length > 0 && voters.every(({ catalog }) => catalog[key]?.trim() !== src)) {\n const names = voters.map(({ locale }) => locale).join(' and ');\n push(out, key, { signal: 'echo', text: `still the source text, and ${names} did translate it` });\n }\n\n // a translation that adds a number is idiomatic (\"refresh\" is \"F5\"); one that changes it is not\n const srcDigits = bag(src, DIGITS, bareDigits);\n const trgDigits = bag(trg, DIGITS, bareDigits);\n if (srcDigits.length > 0 && !same(srcDigits, trgDigits)) {\n push(out, key, { signal: 'digits', text: movedText('says', srcDigits, trgDigits) });\n }\n const srcUrls = bag(src, URLS, trimPunct);\n const trgUrls = bag(trg, URLS, trimPunct);\n if (!same(srcUrls, trgUrls)) {\n push(out, key, { signal: 'url', text: movedText('links to', srcUrls, trgUrls) });\n }\n const srcCode = bag(src, CODE);\n const trgCode = bag(trg, CODE);\n if (!same(srcCode, trgCode)) {\n push(out, key, { signal: 'code', text: movedText('shows the code', srcCode, trgCode) });\n }\n }\n\n return out;\n}\n","import { existsSync } from 'node:fs';\nimport { relative } from 'node:path';\nimport {\n check,\n clearDrafts,\n collectOrigins,\n doctor,\n effectiveDrafts,\n extractProject,\n loadDrafts,\n markDrafts,\n readCatalog,\n saveDrafts,\n status,\n targetLocales,\n validateMessage,\n validatePair,\n writeCatalog,\n} from '@verbaly/compiler';\nimport { DRAFTS_FILE } from '@verbaly/compiler';\nimport type {\n Catalog,\n Catalogs,\n CheckResult,\n DoctorResult,\n Drafts,\n ResolvedConfig,\n StatusResult,\n} from '@verbaly/compiler';\nimport { badRequest } from './http';\nimport { triage, type TriageResult } from './triage';\n\nexport interface StudioProblem {\n scope: string;\n message: string;\n}\n\nexport interface StudioState {\n root: string;\n dir: string;\n sourceLocale: string;\n locales: string[];\n scanning: boolean;\n catalogs: Catalogs;\n origins: Record<string, string[]>;\n status: StatusResult;\n check: CheckResult;\n drafts: Drafts;\n triage: Record<string, TriageResult>;\n problems: StudioProblem[];\n}\n\n// A locale nobody can parse must not take the panel down: Studio is what you open to fix it.\nfunction readAll(cfg: ResolvedConfig): { catalogs: Catalogs; problems: StudioProblem[] } {\n const catalogs: Catalogs = {};\n const problems: StudioProblem[] = [];\n // an empty green panel and a wrong --root look identical, and only this line tells them apart\n if (!existsSync(cfg.dir)) {\n const dir = relative(cfg.root, cfg.dir).replaceAll('\\\\', '/');\n problems.push({ scope: dir, message: '[verbaly] no catalogs here, run npx verbaly init or check --root' });\n }\n for (const locale of cfg.locales) {\n try {\n catalogs[locale] = readCatalog(cfg, locale);\n } catch (error) {\n catalogs[locale] = {};\n problems.push({\n scope: locale,\n message: error instanceof Error ? error.message : String(error),\n });\n }\n }\n return { catalogs, problems };\n}\n\n// A sidecar nobody can parse is the other file Studio exists to fix, so it degrades like a catalog.\nfunction readDrafts(cfg: ResolvedConfig): { drafts: Drafts; problems: StudioProblem[] } {\n try {\n return { drafts: loadDrafts(cfg), problems: [] };\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n return { drafts: {}, problems: [{ scope: DRAFTS_FILE, message }] };\n }\n}\n\n// check() reports the files a key was used in as absolute paths, origins already do not.\nfunction relativize(cfg: ResolvedConfig, result: CheckResult): CheckResult {\n const rel = (file: string): string => relative(cfg.root, file).replaceAll('\\\\', '/');\n return {\n ...result,\n unknown: result.unknown.map((entry) => ({ ...entry, files: entry.files.map(rel) })),\n extra: result.extra.map((entry) => ({ ...entry, files: entry.files.map(rel) })),\n };\n}\n\n// One shot and unpaginated: the biggest catalog we know is past 1600 messages, instant locally.\nexport async function buildState(cfg: ResolvedConfig): Promise<StudioState> {\n const { catalogs, problems } = readAll(cfg);\n const registry = await extractProject(cfg);\n const sidecar = readDrafts(cfg);\n problems.push(...sidecar.problems);\n const live = effectiveDrafts(sidecar.drafts, catalogs);\n const source = (catalogs[cfg.sourceLocale] ?? {}) as Catalog;\n const targets = targetLocales(cfg);\n\n for (const { file, message } of registry.parseErrors()) {\n problems.push({ scope: relative(cfg.root, file).replaceAll('\\\\', '/'), message });\n }\n\n const perLocale: Record<string, TriageResult> = {};\n for (const locale of targets) {\n perLocale[locale] = triage({\n source,\n target: (catalogs[locale] ?? {}) as Catalog,\n // a locale still carrying drafts has not been reviewed, so it cannot be the control\n reviewed: targets\n .filter((other) => other !== locale && !(live[other] ?? []).length)\n .map((other) => ({ locale: other, catalog: (catalogs[other] ?? {}) as Catalog })),\n });\n }\n\n return {\n root: cfg.root,\n dir: cfg.dir,\n sourceLocale: cfg.sourceLocale,\n locales: cfg.locales,\n // with include: [] nothing is scanned, so an empty origins map is off, not \"none found\"\n scanning: cfg.include.length > 0,\n catalogs,\n origins: await collectOrigins(cfg, registry),\n status: status(cfg, catalogs, registry, live),\n check: relativize(cfg, check(cfg, catalogs, registry)),\n drafts: live,\n triage: perLocale,\n problems,\n };\n}\n\nexport function health(cfg: ResolvedConfig): Promise<DoctorResult> {\n return doctor(cfg);\n}\n\nexport interface WriteMessageResult {\n locale: string;\n key: string;\n clearedDraft: boolean;\n}\n\n// A human wrote it, so it is reviewed: the rule import already applies in run.ts:447.\nexport function writeMessage(\n cfg: ResolvedConfig,\n locale: string,\n key: string,\n text: string,\n): WriteMessageResult {\n if (locale === cfg.sourceLocale) {\n throw badRequest('the source text lives in your code, Studio never edits it');\n }\n if (!cfg.locales.includes(locale)) {\n throw badRequest(`${locale} is not one of this project's locales`);\n }\n const source = readCatalog(cfg, cfg.sourceLocale);\n // hasOwn, not `in`: `in` walks the prototype, so toString and __proto__ would pass this guard\n if (!Object.hasOwn(source, key)) {\n throw badRequest(`\"${key}\" is not in the source catalog, so Studio will not create it`);\n }\n // '' is untranslated everywhere else, so whitespace is that and not a translation to validate\n const value = text.trim() ? text : '';\n // the same two calls check() makes: validatePair alone misses a plural block with no other case\n const issues = value\n ? [...validateMessage(value, locale), ...validatePair(source[key] as string, value)]\n : [];\n const errors = issues.filter((issue) => issue.severity === 'error');\n if (errors.length) {\n throw badRequest(`${errors.map((issue) => issue.message).join('; ')}`);\n }\n\n const catalog = { ...readCatalog(cfg, locale), [key]: value };\n writeCatalog(cfg, locale, catalog);\n\n const drafts = loadDrafts(cfg);\n const clearedDraft = (drafts[locale] ?? []).includes(key);\n clearDrafts(drafts, locale, [key]);\n saveDrafts(cfg, drafts);\n return { locale, key, clearedDraft };\n}\n\nexport interface ApproveResult {\n locale: string;\n approved: number;\n keys: string[];\n}\n\n// The sidecar is re-read here: saveDrafts writes every locale, so a stale copy drops another's.\nexport function approve(cfg: ResolvedConfig, locale: string, keys?: string[]): ApproveResult {\n if (!cfg.locales.includes(locale)) {\n throw badRequest(`${locale} is not one of this project's locales`);\n }\n const drafts = loadDrafts(cfg);\n const had = drafts[locale] ?? [];\n // the keys it really cleared, because undo has to put back exactly those and nothing else\n const cleared = keys ? had.filter((key) => keys.includes(key)) : [...had];\n clearDrafts(drafts, locale, keys);\n saveDrafts(cfg, drafts);\n return { locale, approved: cleared.length, keys: cleared };\n}\n\n// Undo is not a nicety here: the flag records who wrote the text, so restoring it restores a fact.\nexport function unapprove(cfg: ResolvedConfig, locale: string, keys: string[]): ApproveResult {\n if (!cfg.locales.includes(locale)) {\n throw badRequest(`${locale} is not one of this project's locales`);\n }\n const drafts = loadDrafts(cfg);\n markDrafts(drafts, locale, keys);\n saveDrafts(cfg, drafts);\n return { locale, approved: keys.length, keys };\n}\n","import { badRequest, HttpError } from './http';\n\nexport type JobState = 'running' | 'done' | 'failed';\n\nexport interface Job {\n id: string;\n kind: 'translate';\n state: JobState;\n total: number;\n done: number;\n locale?: string;\n message?: string;\n result?: unknown;\n written?: number;\n kept?: number;\n}\n\n// One at a time, because two runs over the same catalogs is a race over the same files.\nlet current: Job | null = null;\nlet counter = 0;\n\nexport function running(): Job | null {\n return current?.state === 'running' ? current : null;\n}\n\nexport function read(id: string): Job {\n if (!current || current.id !== id) throw new HttpError(404, `no job called \"${id}\"`);\n return current;\n}\n\nexport function start(kind: Job['kind'], total: number): Job {\n if (running()) throw badRequest('a job is already running, wait for it or read its progress');\n counter += 1;\n current = { id: `${kind}-${counter}`, kind, state: 'running', total, done: 0 };\n return current;\n}\n\n// A batch that failed still finishes: the bar reports work attempted, and message says what broke.\nexport function advance(id: string, keys: number, locale: string, error?: string): void {\n if (current?.id !== id) return;\n current.done = Math.min(current.done + keys, current.total);\n current.locale = locale;\n if (error) current.message = error;\n}\n\n// A run that writes part of its work and then fails is a success for what it wrote, so both land.\nexport function finish(id: string, state: 'done' | 'failed', patch: Partial<Job> = {}): void {\n if (current?.id === id) Object.assign(current, patch, { state });\n}\n","import {\n collectOrigins,\n counted,\n extractProject,\n loadCatalogs,\n mergeTranslations,\n resolveProvider,\n syncCatalogs,\n translateCatalogs,\n writeCatalog,\n writeDts,\n markDrafts,\n loadDrafts,\n saveDrafts,\n} from '@verbaly/compiler';\nimport type { ResolvedConfig } from '@verbaly/compiler';\nimport { badRequest } from './http';\nimport { advance, finish, start, type Job } from './jobs';\n\nexport interface ExtractResult {\n added: Record<string, string[]>;\n // only the source locale counts as new text: a target gains a blank for every gap it had\n found: number;\n messages: number;\n}\n\n// Local, fast and free, so it answers in the same request instead of becoming a job.\nexport async function runExtract(cfg: ResolvedConfig): Promise<ExtractResult> {\n if (!cfg.include.length) {\n throw badRequest('source scanning is off (include: []), so there is nothing to extract');\n }\n const registry = await extractProject(cfg);\n const catalogs = loadCatalogs(cfg);\n const result = syncCatalogs(cfg, catalogs, registry);\n for (const locale of cfg.locales) writeCatalog(cfg, locale, catalogs[locale] ?? {});\n // the types come from the source catalog; writeDts skips them when the project set dts: false\n writeDts(cfg, catalogs[cfg.sourceLocale] ?? {});\n const found = result.added[cfg.sourceLocale]?.length ?? 0;\n return { added: result.added, found, messages: registry.messages().size };\n}\n\nexport interface Plan {\n pending: Record<string, string[]>;\n total: number;\n}\n\n// translate is the one command that spends money and minutes, so the panel always shows the bill.\nexport async function planTranslate(cfg: ResolvedConfig, locales?: string[]): Promise<Plan> {\n const provider = await resolveProvider(cfg);\n const catalogs = loadCatalogs(cfg);\n const result = await translateCatalogs(cfg, catalogs, provider, { locales, dryRun: true });\n const total = Object.values(result.pending).reduce((n, keys) => n + keys.length, 0);\n return { pending: result.pending, total };\n}\n\nexport async function startTranslate(cfg: ResolvedConfig, locales?: string[]): Promise<Job> {\n const provider = await resolveProvider(cfg);\n // the job carries its own denominator, so progress reads the same whether or not you asked first\n const plan = await planTranslate(cfg, locales);\n const job = start('translate', plan.total);\n const id = job.id;\n\n // the run outlives this request on purpose: the client asks the job how it is going\n void (async () => {\n try {\n const catalogs = loadCatalogs(cfg);\n const result = await translateCatalogs(cfg, catalogs, provider, {\n locales,\n batchSize: cfg.translate.batchSize,\n concurrency: cfg.translate.concurrency,\n retries: cfg.translate.retries,\n origins: await collectOrigins(cfg),\n onProgress: (p) => advance(id, p.keys, p.locale, p.error),\n });\n // what a machine wrote stays a draft: the panel does not get to change that rule\n const drafts = loadDrafts(cfg);\n let written = 0;\n let kept = 0;\n for (const [locale, keys] of Object.entries(result.translated)) {\n const landed = mergeTranslations(cfg, locale, catalogs[locale] ?? {}, keys);\n markDrafts(drafts, locale, landed);\n written += landed.length;\n kept += keys.length - landed.length;\n }\n saveDrafts(cfg, drafts);\n // on a failure the panel shows only this line, so it says what landed and what did not\n const failed = result.failed[0];\n finish(id, failed ? 'failed' : 'done', {\n result,\n written,\n kept,\n message: failed\n ? `${counted(written, 'message')} translated, but ${counted(result.failed.length, 'batch', 'batches')} did not answer (${failed.error}): run it again for the rest`\n : undefined,\n });\n } catch (error) {\n finish(id, 'failed', { message: error instanceof Error ? error.message : String(error) });\n }\n })();\n\n return job;\n}\n","import { execFile, spawn } from 'node:child_process';\nimport { promisify } from 'node:util';\nimport { join, relative } from 'node:path';\nimport { parseCatalog, type Catalog, type ResolvedConfig } from '@verbaly/compiler';\n\nconst run = promisify(execFile);\n\nexport interface Commit {\n sha: string;\n author: string;\n date: string;\n subject: string;\n}\n\n// one field separator that no subject can contain, written as the escape and never as the byte\nconst FIELD = '\\u001f';\nconst FORMAT = `--format=%H${FIELD}%an${FIELD}%aI${FIELD}%s`;\n\n// the full sha is what git resolves without ambiguity, and seven characters is what a card shows\nexport function parseFullLog(stdout: string): Commit[] {\n const commits: Commit[] = [];\n for (const line of stdout.split('\\n')) {\n const [sha, author, date, ...rest] = line.split(FIELD);\n // a subject holds anything but a newline, so it is the last field and is rejoined\n if (!sha || !author || !date || !rest.length) continue;\n commits.push({ sha, author, date, subject: rest.join(FIELD) });\n }\n return commits;\n}\n\n// split out so the shape of a log line is tested without a repository and without writing one\nexport function parseLog(stdout: string): Commit[] {\n return parseFullLog(stdout).map((commit) => ({ ...commit, sha: commit.sha.slice(0, 7) }));\n}\n\n// git is asked, never assumed: no repository and no git both mean there is no history to show\nexport async function catalogHistory(cfg: ResolvedConfig, limit = 5): Promise<Commit[]> {\n const dir = relative(cfg.root, cfg.dir).replaceAll('\\\\', '/') || '.';\n try {\n const { stdout } = await run('git', ['log', `-n${limit}`, FORMAT, '--', dir], {\n cwd: cfg.root,\n timeout: 3000,\n windowsHide: true,\n });\n return parseLog(stdout);\n } catch {\n return [];\n }\n}\n\nexport interface Revision {\n commit: Commit;\n now: Catalog | undefined;\n before: Catalog | undefined;\n}\n\n// blame measured 26% wrong on verbaly-web: a sibling's comma rewrites a line, a value does not\nexport function attribute(\n revisions: Revision[],\n owners = new Map<string, Commit>(),\n): Map<string, Commit> {\n for (const { commit, now, before } of revisions) {\n if (!now || !before) continue;\n for (const key of new Set([...Object.keys(now), ...Object.keys(before)])) {\n if (!owners.has(key) && now[key] !== before[key]) owners.set(key, commit);\n }\n }\n return owners;\n}\n\n// a key can move in more than one catalog, so the answer is the newest of the per-file answers\nexport function mergeOwners(maps: Map<string, Commit>[]): Map<string, Commit> {\n const merged = new Map<string, Commit>();\n for (const map of maps) {\n for (const [key, commit] of map) {\n const held = merged.get(key);\n if (!held || Date.parse(commit.date) > Date.parse(held.date)) merged.set(key, commit);\n }\n }\n return merged;\n}\n\nconst BLOB = /^[0-9a-f]{40} \\w+ (\\d+)$/;\n\n// git cat-file --batch answers every revision in one process: a header line, then that many bytes\nfunction readBlobs(\n root: string,\n requests: string[],\n take: (index: number, body: string | null, blob: string) => void,\n): Promise<boolean> {\n return new Promise((resolve) => {\n const child = spawn('git', ['cat-file', '--batch'], { cwd: root, windowsHide: true });\n // a killed batch leaves a half map, and a half map answers \"never committed\" for the rest\n const stop = setTimeout(() => {\n ok = false;\n child.kill();\n }, 20000);\n let pending: Buffer = Buffer.alloc(0);\n let index = 0;\n let ok = true;\n const fail = () => {\n ok = false;\n };\n child.on('error', fail);\n child.stdin.on('error', fail);\n child.stdout.on('data', (chunk: Buffer) => {\n pending = pending.length ? Buffer.concat([pending, chunk]) : chunk;\n for (;;) {\n const end = pending.indexOf(10);\n if (end < 0) break;\n const header = pending.toString('utf8', 0, end);\n const blob = BLOB.exec(header);\n // anything but a blob header is a revision git could not resolve, which is an answer too\n if (!blob) {\n take(index++, null, '');\n pending = pending.subarray(end + 1);\n continue;\n }\n const size = Number(blob[1]);\n if (pending.length < end + size + 2) break;\n take(index++, pending.toString('utf8', end + 1, end + 1 + size), header.slice(0, 40));\n pending = pending.subarray(end + size + 2);\n }\n });\n child.on('close', () => {\n clearTimeout(stop);\n resolve(ok);\n });\n child.stdin.end(requests.join('\\n') + '\\n');\n });\n}\n\nexport interface KeyHistory {\n owners: Map<string, Commit>;\n committed: Set<string>;\n ok: boolean;\n}\n\n// the window a local tool is willing to read: past this, the answer is \"older than what I read\"\nconst WINDOW = 200;\n\nasync function walkFile(root: string, file: string, limit: number) {\n const { stdout } = await run('git', ['log', `-n${limit}`, FORMAT, '--', file], {\n cwd: root,\n timeout: 5000,\n windowsHide: true,\n maxBuffer: 8_000_000,\n });\n const commits = parseFullLog(stdout);\n if (!commits.length) return { owners: new Map<string, Commit>(), head: [] as string[] };\n\n const owners = new Map<string, Commit>();\n let head: string[] = [];\n let waiting: Catalog | undefined;\n // only two flat catalogs are ever live, and the pair shares a blob, so one entry caches it\n let last: { blob: string; flat: Catalog | undefined } | null = null;\n const parse = (body: string | null, blob: string): Catalog | undefined => {\n if (body === null) return {};\n if (last && last.blob === blob) return last.flat;\n const flat = parseCatalog(body);\n last = { blob, flat };\n return flat;\n };\n\n const whole = await readBlobs(\n root,\n commits.flatMap((commit) => [`${commit.sha}:${file}`, `${commit.sha}^:${file}`]),\n (index, body, blob) => {\n const flat = parse(body, blob);\n if (index % 2 === 0) {\n waiting = flat;\n if (index === 0 && flat) head = Object.keys(flat);\n return;\n }\n const commit = commits[(index - 1) / 2];\n if (commit) attribute([{ commit, now: waiting, before: flat }], owners);\n waiting = undefined;\n },\n );\n if (!whole) throw new Error('[verbaly] git could not read every revision of this catalog');\n return { owners, head };\n}\n\n// The whole map at once, because a per-key query costs a git process and every row has a menu.\nexport async function catalogKeyHistory(\n cfg: ResolvedConfig,\n limit = WINDOW,\n): Promise<KeyHistory> {\n const maps: Map<string, Commit>[] = [];\n const committed = new Set<string>();\n let ok = false;\n for (const locale of cfg.locales) {\n const file = relative(cfg.root, join(cfg.dir, `${locale}.json`)).replaceAll('\\\\', '/');\n try {\n const walked = await walkFile(cfg.root, file, limit);\n maps.push(walked.owners);\n for (const key of walked.head) committed.add(key);\n ok = true;\n } catch {\n // one unreadable locale is not the end of the walk: the others still answer for their keys\n }\n }\n return { owners: mergeOwners(maps), committed, ok };\n}\n\nexport interface KeyCommit {\n key: string;\n commit: Commit | null;\n reason?: 'nogit' | 'uncommitted' | 'older';\n}\n\nconst walked = new Map<string, { head: string; history: KeyHistory }>();\n\nasync function headSha(root: string): Promise<string> {\n try {\n const { stdout } = await run('git', ['rev-parse', 'HEAD'], {\n cwd: root,\n timeout: 3000,\n windowsHide: true,\n });\n return stdout.trim();\n } catch {\n return '';\n }\n}\n\n// HEAD moving is the only thing that can change an answer, so it is what invalidates the walk\nasync function keyHistory(cfg: ResolvedConfig): Promise<KeyHistory> {\n const head = await headSha(cfg.root);\n const held = walked.get(cfg.root);\n if (held && held.head === head) return held.history;\n const history = await catalogKeyHistory(cfg);\n walked.set(cfg.root, { head, history });\n return history;\n}\n\nexport async function keyCommit(cfg: ResolvedConfig, key: string): Promise<KeyCommit> {\n const history = await keyHistory(cfg);\n if (!history.ok) return { key, commit: null, reason: 'nogit' };\n const commit = history.owners.get(key);\n // seven characters is what a person reads and pastes, and the walk needed the full one\n if (commit) return { key, commit: { ...commit, sha: commit.sha.slice(0, 7) } };\n return { key, commit: null, reason: history.committed.has(key) ? 'older' : 'uncommitted' };\n}\n","import { existsSync, readFileSync, statSync } from 'node:fs';\nimport { join, normalize, resolve } from 'node:path';\nimport { fileURLToPath } from 'node:url';\n\n// the built panel sits beside the compiled server, so it is found relative to this file, never cwd\nconst UI = resolve(fileURLToPath(new URL('.', import.meta.url)), 'ui');\n\nconst TYPES: Record<string, string> = {\n '.html': 'text/html; charset=utf-8',\n '.js': 'text/javascript; charset=utf-8',\n '.css': 'text/css; charset=utf-8',\n '.svg': 'image/svg+xml',\n '.woff2': 'font/woff2',\n '.json': 'application/json; charset=utf-8',\n '.ico': 'image/x-icon',\n};\n\nexport interface Asset {\n body: Buffer;\n type: string;\n}\n\nexport const uiBuilt = (): boolean => existsSync(join(UI, 'index.html'));\n\n// A path is resolved and then checked to still be inside UI: ../ and an absolute one both land out.\nexport function readAsset(pathname: string): Asset | undefined {\n const wanted = pathname === '/' ? '/index.html' : pathname;\n const target = resolve(join(UI, normalize(wanted)));\n if (target !== UI && !target.startsWith(UI + (process.platform === 'win32' ? '\\\\' : '/'))) {\n return undefined;\n }\n if (!existsSync(target) || !statSync(target).isFile()) return undefined;\n const dot = target.lastIndexOf('.');\n return {\n body: readFileSync(target),\n type: TYPES[target.slice(dot).toLowerCase()] ?? 'application/octet-stream',\n };\n}\n","import { randomBytes, timingSafeEqual } from 'node:crypto';\n\nexport interface GuardOptions {\n port: number;\n token: string;\n}\n\n// A local server that writes files is reachable from any page the browser has open.\nexport function newToken(): string {\n return randomBytes(9).toString('base64url');\n}\n\n// Only the two names that resolve to this machine. A DNS rebind arrives with a foreign Host.\nexport function hostAllowed(host: string | undefined, port: number): boolean {\n // a browser omits the port when it is the scheme default, so :80 would 403 every real request\n const names = port === 80 ? ['127.0.0.1', 'localhost'] : [];\n return [`127.0.0.1:${port}`, `localhost:${port}`, ...names].includes(host ?? '');\n}\n\n// No Origin at all is a same-origin navigation or a curl, both fine; a foreign one never is.\nexport function originAllowed(origin: string | undefined, port: number): boolean {\n if (origin === undefined) return true;\n return origin === `http://127.0.0.1:${port}` || origin === `http://localhost:${port}`;\n}\n\n// Compared in constant time so the token cannot be guessed one character at a time.\nexport function tokenMatches(given: string | null, expected: string): boolean {\n if (given === null) return false;\n const a = Buffer.from(given);\n const b = Buffer.from(expected);\n return a.length === b.length && timingSafeEqual(a, b);\n}\n\nexport interface GuardVerdict {\n ok: boolean;\n status: number;\n reason: string;\n}\n\nconst PASS: GuardVerdict = { ok: true, status: 200, reason: '' };\n\nexport function guardRequest(\n headers: { host?: string; origin?: string },\n url: URL,\n { port, token }: GuardOptions,\n): GuardVerdict {\n if (!hostAllowed(headers.host, port)) {\n return { ok: false, status: 403, reason: '[verbaly] unexpected Host header' };\n }\n if (!originAllowed(headers.origin, port)) {\n return { ok: false, status: 403, reason: '[verbaly] cross-origin request' };\n }\n if (!tokenMatches(url.searchParams.get('t'), token)) {\n return { ok: false, status: 401, reason: '[verbaly] missing or wrong token, open the printed url' };\n }\n return PASS;\n}\n","import { createServer, type IncomingMessage, type Server, type ServerResponse } from 'node:http';\nimport type { AddressInfo } from 'node:net';\nimport type { ResolvedConfig } from '@verbaly/compiler';\nimport { approve, buildState, health, unapprove, writeMessage } from './api';\nimport { planTranslate, runExtract, startTranslate } from './actions';\nimport { read as readJob, running } from './jobs';\nimport { catalogHistory, keyCommit } from './history';\nimport { readAsset, uiBuilt } from './assets';\nimport { guardRequest, newToken } from './guard';\nimport { HttpError, badRequest, scrub } from './http';\n\nexport interface StudioServer {\n server: Server;\n port: number;\n token: string;\n url: string;\n close: () => Promise<void>;\n}\n\nexport interface StartOptions {\n port?: number;\n token?: string;\n}\n\nconst DEFAULT_PORT = 4747;\nconst MAX_BODY = 1_000_000;\n\nfunction send(res: ServerResponse, status: number, body: unknown): void {\n res.writeHead(status, {\n 'content-type': 'application/json; charset=utf-8',\n 'cache-control': 'no-store',\n });\n res.end(JSON.stringify(body));\n}\n\nasync function readJson(req: IncomingMessage): Promise<Record<string, unknown>> {\n const chunks: Buffer[] = [];\n let size = 0;\n for await (const chunk of req) {\n size += (chunk as Buffer).length;\n if (size > MAX_BODY) throw new HttpError(413, 'request body too large');\n chunks.push(chunk as Buffer);\n }\n if (!chunks.length) return {};\n let parsed: unknown;\n try {\n parsed = JSON.parse(Buffer.concat(chunks).toString('utf8'));\n } catch {\n throw badRequest('body is not valid JSON');\n }\n // null and an array both pass typeof 'object', and reading .text off them is a 500\n if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {\n throw badRequest('body must be a JSON object');\n }\n return parsed as Record<string, unknown>;\n}\n\nfunction segment(raw: string | undefined, what: string): string {\n let value: string;\n try {\n value = decodeURIComponent(raw ?? '');\n } catch {\n throw badRequest(`the ${what} in the url is not valid percent-encoding`);\n }\n if (!value) throw badRequest(`the url is missing a ${what}`);\n return value;\n}\n\n// One page, JSON on every route: a framework would only add a dependency to a local tool.\nexport function createStudioApp(cfg: ResolvedConfig, port: number, token: string) {\n return async function handle(req: IncomingMessage, res: ServerResponse): Promise<void> {\n // The url is built inside the try: `GET //` throws, and out here that would kill the process.\n try {\n let url: URL;\n try {\n url = new URL(req.url ?? '/', `http://127.0.0.1:${port}`);\n } catch {\n throw badRequest('the request target is not a valid url');\n }\n\n const api = url.pathname.startsWith('/api/');\n const verdict = guardRequest({ host: req.headers.host, origin: req.headers.origin }, url, {\n port,\n token,\n });\n // the panel files are the published npm package: only the project data needs a token\n if (!verdict.ok && (api || verdict.status !== 401)) {\n send(res, verdict.status, { error: verdict.reason });\n return;\n }\n\n if (!api && req.method === 'GET') {\n const asset = readAsset(url.pathname);\n if (asset) {\n res.writeHead(200, { 'content-type': asset.type, 'cache-control': 'no-store' });\n res.end(asset.body);\n return;\n }\n if (url.pathname === '/' && !uiBuilt()) {\n throw new HttpError(500, 'the panel is not in this install, so only /api/* answers here');\n }\n }\n\n if (req.method === 'GET' && url.pathname === '/api/state') {\n send(res, 200, await buildState(cfg));\n return;\n }\n if (req.method === 'GET' && url.pathname === '/api/history') {\n send(res, 200, { commits: await catalogHistory(cfg) });\n return;\n }\n if (req.method === 'GET' && url.pathname.startsWith('/api/commit/')) {\n const key = segment(url.pathname.slice('/api/commit/'.length), 'key');\n send(res, 200, await keyCommit(cfg, key));\n return;\n }\n if (req.method === 'GET' && url.pathname === '/api/health') {\n send(res, 200, await health(cfg));\n return;\n }\n if (req.method === 'PUT' && url.pathname.startsWith('/api/message/')) {\n const rest = url.pathname.slice('/api/message/'.length).split('/');\n const locale = segment(rest[0], 'locale');\n const key = segment(rest.slice(1).join('/'), 'key');\n const body = await readJson(req);\n if (typeof body.text !== 'string') throw badRequest('body needs a \"text\" string');\n send(res, 200, writeMessage(cfg, locale, key, body.text));\n return;\n }\n if (req.method === 'POST' && url.pathname === '/api/approve') {\n const body = await readJson(req);\n if (typeof body.locale !== 'string') throw badRequest('body needs a \"locale\" string');\n const keys = Array.isArray(body.keys)\n ? body.keys.filter((k): k is string => typeof k === 'string')\n : undefined;\n if (body.undo === true) {\n if (!keys?.length) throw badRequest('undoing needs the \"keys\" it should put back');\n send(res, 200, unapprove(cfg, body.locale, keys));\n return;\n }\n send(res, 200, approve(cfg, body.locale, keys));\n return;\n }\n if (req.method === 'POST' && url.pathname === '/api/extract') {\n send(res, 200, await runExtract(cfg));\n return;\n }\n if (req.method === 'GET' && url.pathname === '/api/translate') {\n const locales = url.searchParams.getAll('locale');\n send(res, 200, await planTranslate(cfg, locales.length ? locales : undefined));\n return;\n }\n if (req.method === 'POST' && url.pathname === '/api/translate') {\n const body = await readJson(req);\n const locales = Array.isArray(body.locales)\n ? body.locales.filter((l): l is string => typeof l === 'string')\n : undefined;\n send(res, 200, await startTranslate(cfg, locales?.length ? locales : undefined));\n return;\n }\n // a reload loses the id, so the panel asks what is running rather than losing the bar\n if (req.method === 'GET' && url.pathname === '/api/job') {\n send(res, 200, running());\n return;\n }\n if (req.method === 'GET' && url.pathname.startsWith('/api/job/')) {\n send(res, 200, readJob(segment(url.pathname.slice('/api/job/'.length), 'job id')));\n return;\n }\n\n send(res, 404, { error: `[verbaly] no route for ${req.method} ${url.pathname}` });\n } catch (error) {\n const status = error instanceof HttpError ? error.status : 500;\n const message = error instanceof Error ? error.message : String(error);\n send(res, status, { error: scrub(message, cfg.root) });\n }\n };\n}\n\n// A busy port climbs instead of failing: this is a local tool, not a service on a fixed address.\nfunction listen(server: Server, port: number, attemptsLeft: number): Promise<number> {\n return new Promise((resolve, reject) => {\n const onError = (error: NodeJS.ErrnoException) => {\n if (error.code === 'EADDRINUSE' && attemptsLeft > 0) {\n server.removeListener('error', onError);\n resolve(listen(server, port + 1, attemptsLeft - 1));\n return;\n }\n reject(error);\n };\n server.once('error', onError);\n server.listen(port, '127.0.0.1', () => {\n server.removeListener('error', onError);\n // port 0 asks the system to pick, so the bound address is the only place the answer is\n const bound = server.address() as AddressInfo | string | null;\n resolve(typeof bound === 'object' && bound !== null ? bound.port : port);\n });\n });\n}\n\nexport async function startStudio(\n cfg: ResolvedConfig,\n options: StartOptions = {},\n): Promise<StudioServer> {\n const token = options.token ?? newToken();\n const server = createServer();\n // The handler is attached before listen so no request can arrive without one.\n let port = options.port ?? DEFAULT_PORT;\n server.on('request', (req, res) => void createStudioApp(cfg, port, token)(req, res));\n port = await listen(server, port, 20);\n\n return {\n server,\n port,\n token,\n url: `http://127.0.0.1:${port}/?t=${token}`,\n close: () =>\n new Promise<void>((resolve, reject) =>\n server.close((error) => (error ? reject(error) : resolve())),\n ),\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":["readJob"],"sources":["../src/http.ts","../src/triage.ts","../src/api.ts","../src/jobs.ts","../src/actions.ts","../src/history.ts","../src/assets.ts","../src/guard.ts","../src/server.ts"],"sourcesContent":["// A client mistake is not a server fault, so the status rides with the error, not a blanket 500.\nexport class HttpError extends Error {\n readonly status: number;\n\n constructor(status: number, message: string) {\n super(message.startsWith('[verbaly]') ? message : `[verbaly] ${message}`);\n this.status = status;\n this.name = 'HttpError';\n }\n}\n\nexport const badRequest = (message: string) => new HttpError(400, message);\n\n// The absolute path of someone's project is not the browser's business.\nexport function scrub(message: string, root: string): string {\n return root ? message.split(root).join('.') : message;\n}\n","import type { Catalog } from '@verbaly/compiler';\n\nexport type TriageSignal = 'divergent' | 'collision' | 'echo' | 'digits' | 'code' | 'url';\n\nexport interface TriageReason {\n signal: TriageSignal;\n text: string;\n}\n\nexport type TriageResult = Record<string, TriageReason[]>;\n\n// check() already caught the structure, so these only point at the few worth reading first.\nexport interface TriageInput {\n source: Catalog;\n target: Catalog;\n reviewed: ReviewedLocale[];\n}\n\n// the locale rides with the catalog because a reason that cannot name the language explains nothing\nexport interface ReviewedLocale {\n locale: string;\n catalog: Catalog;\n}\n\nconst DIGITS = /\\d[\\d.,]*/g;\nconst URLS = /https?:\\/\\/[^\\s<)\"']+/g;\nconst CODE = /<code>([^<]+)<\\/code>/g;\n\nfunction bag(text: string, re: RegExp, normalize?: (v: string) => string): string[] {\n const found = text.match(re) ?? [];\n return (normalize ? found.map(normalize) : found).sort();\n}\n\nfunction same(a: string[], b: string[]): boolean {\n return a.length === b.length && a.every((v, i) => v === b[i]);\n}\n\n// A number or a url at the end of a clause takes the punctuation with it, and that is not a signal.\nconst trimPunct = (v: string) => v.replace(/[.,;:!?)\\]]+$/, '');\n// A separator is idiomatic (3.09 is 3,09 in Spanish); canonizing keeps 1.5 apart from 15.\nconst bareDigits = (v: string) => trimPunct(v).replaceAll(/[.,]/g, '.');\n\nfunction push(out: TriageResult, key: string, reason: TriageReason): void {\n (out[key] ??= []).push(reason);\n}\n\n// the column is three short lines, so a reason quotes the evidence and never the whole message\nfunction clip(text: string, max = 42): string {\n return text.length > max ? `${text.slice(0, max - 1)}…` : text;\n}\n\nconst quote = (text: string, max?: number) => `\"${clip(text, max)}\"`;\n\n// what the source has and the translation does not, which is the half worth naming\nfunction lost(from: string[], to: string[]): string[] {\n const rest = [...to];\n const out: string[] = [];\n for (const item of from) {\n const at = rest.indexOf(item);\n if (at < 0) out.push(item);\n else rest.splice(at, 1);\n }\n return out;\n}\n\n// \"the numbers moved\" is a label; naming the one that moved is what saves the reader a diff\nfunction movedText(kind: string, from: string[], to: string[]): string {\n const gone = lost(from, to);\n const added = lost(to, from);\n if (gone.length && added.length) {\n return `the source ${kind} ${quote(gone[0]!)}, this one ${kind} ${quote(added[0]!)}`;\n }\n if (gone.length) return `the source ${kind} ${quote(gone[0]!)} and this one does not`;\n return `this one ${kind} ${quote(added[0]!)} and the source does not`;\n}\n\nexport function triage({ source, target, reviewed }: TriageInput): TriageResult {\n // null-prototype: a source text of \"__proto__\" or \"toString\" is a string, not a member of Object\n const out: TriageResult = Object.create(null);\n const keys = Object.keys(source);\n\n // One source text that two humans rendered two ways is ambiguous, so the machine had no chance.\n const bySource: Record<string, string[]> = Object.create(null);\n for (const key of keys) {\n const text = source[key]?.trim();\n if (text) (bySource[text] ??= []).push(key);\n }\n for (const [text, group] of Object.entries(bySource)) {\n if (group.length < 2) continue;\n\n // the first reviewed locale that disagreed with itself, and the two renderings it used\n let split: { locale: string; first: string; second: string } | undefined;\n for (const { locale, catalog } of reviewed) {\n const seen = [...new Set(group.map((k) => catalog[k]?.trim() ?? '').filter(Boolean))];\n if (seen.length > 1) {\n split = { locale, first: seen[0]!, second: seen[1]! };\n break;\n }\n }\n if (split) {\n for (const key of group) {\n push(out, key, {\n signal: 'divergent',\n text: `${split.locale} already says this two ways: ${quote(split.first, 32)} and ${quote(split.second, 32)}`,\n });\n }\n }\n\n // The same ambiguity seen from the result instead of from the history.\n const targets = new Set(group.map((key) => target[key]?.trim() ?? '').filter(Boolean));\n if (!split && targets.size > 1) {\n for (const key of group) {\n const mine = target[key]?.trim() ?? '';\n const other = group.find((k) => k !== key && (target[k]?.trim() ?? '') !== mine && target[k]?.trim());\n push(out, key, {\n signal: 'collision',\n text: other\n ? `${other} has the same source text and says ${quote(target[other]!.trim())}`\n : `${quote(text)} came back as two different translations`,\n });\n }\n }\n }\n\n for (const key of keys) {\n const src = source[key]?.trim() ?? '';\n const trg = target[key]?.trim() ?? '';\n if (!src || !trg) continue;\n\n // Only the locales with an opinion on this key vote: alone it fires on every do-not-translate.\n const voters = reviewed.filter(({ catalog }) => {\n const value = catalog[key]?.trim();\n return value !== undefined && value !== '';\n });\n if (trg === src && voters.length > 0 && voters.every(({ catalog }) => catalog[key]?.trim() !== src)) {\n const names = voters.map(({ locale }) => locale).join(' and ');\n push(out, key, { signal: 'echo', text: `still the source text, and ${names} did translate it` });\n }\n\n // a translation that adds a number is idiomatic (\"refresh\" is \"F5\"); one that changes it is not\n const srcDigits = bag(src, DIGITS, bareDigits);\n const trgDigits = bag(trg, DIGITS, bareDigits);\n if (srcDigits.length > 0 && !same(srcDigits, trgDigits)) {\n push(out, key, { signal: 'digits', text: movedText('says', srcDigits, trgDigits) });\n }\n const srcUrls = bag(src, URLS, trimPunct);\n const trgUrls = bag(trg, URLS, trimPunct);\n if (!same(srcUrls, trgUrls)) {\n push(out, key, { signal: 'url', text: movedText('links to', srcUrls, trgUrls) });\n }\n const srcCode = bag(src, CODE);\n const trgCode = bag(trg, CODE);\n if (!same(srcCode, trgCode)) {\n push(out, key, { signal: 'code', text: movedText('shows the code', srcCode, trgCode) });\n }\n }\n\n return out;\n}\n","import { existsSync } from 'node:fs';\nimport { relative } from 'node:path';\nimport {\n check,\n clearDrafts,\n collectOrigins,\n doctor,\n effectiveDrafts,\n extractProject,\n loadDrafts,\n loadState,\n markDrafts,\n readCatalog,\n readState,\n recordTranslations,\n saveDrafts,\n shippedCatalogs,\n status,\n targetLocales,\n validateMessage,\n validatePair,\n writeCatalog,\n} from '@verbaly/compiler';\nimport { STATE_FILE } from '@verbaly/compiler';\nimport type {\n Catalog,\n Catalogs,\n CheckResult,\n DoctorResult,\n Drafts,\n ResolvedConfig,\n StatusResult,\n} from '@verbaly/compiler';\nimport { badRequest } from './http';\nimport { triage, type TriageResult } from './triage';\n\nexport interface StudioProblem {\n scope: string;\n message: string;\n}\n\nexport interface StudioState {\n root: string;\n dir: string;\n sourceLocale: string;\n locales: string[];\n scanning: boolean;\n catalogs: Catalogs;\n origins: Record<string, string[]>;\n status: StatusResult;\n check: CheckResult;\n drafts: Drafts;\n triage: Record<string, TriageResult>;\n problems: StudioProblem[];\n}\n\n// A locale nobody can parse must not take the panel down: Studio is what you open to fix it.\nfunction readAll(cfg: ResolvedConfig): { catalogs: Catalogs; problems: StudioProblem[] } {\n const catalogs: Catalogs = {};\n const problems: StudioProblem[] = [];\n // an empty green panel and a wrong --root look identical, and only this line tells them apart\n if (!existsSync(cfg.dir)) {\n const dir = relative(cfg.root, cfg.dir).replaceAll('\\\\', '/');\n problems.push({ scope: dir, message: '[verbaly] no catalogs here, run npx verbaly init or check --root' });\n }\n for (const locale of cfg.locales) {\n try {\n catalogs[locale] = readCatalog(cfg, locale);\n } catch (error) {\n catalogs[locale] = {};\n problems.push({\n scope: locale,\n message: error instanceof Error ? error.message : String(error),\n });\n }\n }\n return { catalogs, problems };\n}\n\n\n// check() reports the files a key was used in as absolute paths, origins already do not.\nfunction relativize(cfg: ResolvedConfig, result: CheckResult): CheckResult {\n const rel = (file: string): string => relative(cfg.root, file).replaceAll('\\\\', '/');\n return {\n ...result,\n unknown: result.unknown.map((entry) => ({ ...entry, files: entry.files.map(rel) })),\n extra: result.extra.map((entry) => ({ ...entry, files: entry.files.map(rel) })),\n collisions: result.collisions.map((entry) => ({\n ...entry,\n sites: entry.sites.map((at) => ({ ...at, file: rel(at.file) })),\n })),\n divergent: result.divergent.map((entry) => ({\n ...entry,\n code: { ...entry.code, file: rel(entry.code.file) },\n })),\n };\n}\n\n// One shot and unpaginated: the biggest catalog we know is past 1600 messages, instant locally.\nexport async function buildState(cfg: ResolvedConfig): Promise<StudioState> {\n const { catalogs, problems } = readAll(cfg);\n const registry = await extractProject(cfg);\n // a sidecar nobody can parse is the other file Studio is there to fix: it degrades like a catalog\n const { state: sidecar, problem } = readState(cfg);\n if (problem) problems.push({ scope: STATE_FILE, message: problem });\n const live = effectiveDrafts(sidecar.drafts, catalogs);\n const source = (catalogs[cfg.sourceLocale] ?? {}) as Catalog;\n const targets = targetLocales(cfg);\n\n for (const { file, message } of registry.parseErrors()) {\n problems.push({ scope: relative(cfg.root, file).replaceAll('\\\\', '/'), message });\n }\n\n const perLocale: Record<string, TriageResult> = {};\n for (const locale of targets) {\n perLocale[locale] = triage({\n source,\n target: (catalogs[locale] ?? {}) as Catalog,\n // a locale still carrying drafts has not been reviewed, so it cannot be the control\n reviewed: targets\n .filter((other) => other !== locale && !(live[other] ?? []).length)\n .map((other) => ({ locale: other, catalog: (catalogs[other] ?? {}) as Catalog })),\n });\n }\n\n return {\n root: cfg.root,\n dir: cfg.dir,\n sourceLocale: cfg.sourceLocale,\n locales: cfg.locales,\n // with include: [] nothing is scanned, so an empty origins map is off, not \"none found\"\n scanning: cfg.include.length > 0,\n catalogs,\n origins: await collectOrigins(cfg, registry),\n status: status(cfg, catalogs, registry, live, sidecar.fingerprints),\n check: relativize(cfg, check(cfg, catalogs, registry, sidecar.fingerprints)),\n drafts: live,\n triage: perLocale,\n problems,\n };\n}\n\nexport function health(cfg: ResolvedConfig): Promise<DoctorResult> {\n return doctor(cfg);\n}\n\nexport interface WriteMessageResult {\n locale: string;\n key: string;\n clearedDraft: boolean;\n}\n\n// A human wrote it, so it is reviewed and stamped for the text that ships, as import does.\nexport async function writeMessage(\n cfg: ResolvedConfig,\n locale: string,\n key: string,\n text: string,\n): Promise<WriteMessageResult> {\n if (locale === cfg.sourceLocale) {\n throw badRequest('the source text lives in your code, Studio never edits it');\n }\n if (!cfg.locales.includes(locale)) {\n throw badRequest(`${locale} is not one of this project's locales`);\n }\n // the review state is written after the catalog: an unreadable one stops the save before it\n const before = loadState(cfg);\n const catalogs = { [cfg.sourceLocale]: readCatalog(cfg, cfg.sourceLocale) };\n const registry = await extractProject(cfg);\n // checked against the text that ships, the one check reads every translation against\n const source = shippedCatalogs(cfg, catalogs, registry, { newKeys: false })[cfg.sourceLocale]!;\n // hasOwn, not `in`: `in` walks the prototype, so toString and __proto__ would pass this guard\n if (!Object.hasOwn(source, key)) {\n throw badRequest(`\"${key}\" is not in the source catalog, so Studio will not create it`);\n }\n // '' is untranslated everywhere else, so whitespace is that and not a translation to validate\n const value = text.trim() ? text : '';\n // the same two calls check() makes: validatePair alone misses a plural block with no other case\n const issues = value\n ? [...validateMessage(value, locale), ...validatePair(source[key] as string, value)]\n : [];\n const errors = issues.filter((issue) => issue.severity === 'error');\n if (errors.length) {\n throw badRequest(`${errors.map((issue) => issue.message).join('; ')}`);\n }\n\n const catalog = { ...readCatalog(cfg, locale), [key]: value };\n writeCatalog(cfg, locale, catalog);\n\n const clearedDraft = (before.drafts[locale] ?? []).includes(key);\n recordTranslations(cfg, source, [{ locale, entries: [{ key, text: value }], draft: false }]);\n return { locale, key, clearedDraft };\n}\n\nexport interface ApproveResult {\n locale: string;\n approved: number;\n keys: string[];\n}\n\n// The sidecar is re-read here: saveDrafts writes every locale, so a stale copy drops another's.\nexport function approve(cfg: ResolvedConfig, locale: string, keys?: string[]): ApproveResult {\n if (!cfg.locales.includes(locale)) {\n throw badRequest(`${locale} is not one of this project's locales`);\n }\n const drafts = loadDrafts(cfg);\n const had = drafts[locale] ?? [];\n // the keys it really cleared, because undo has to put back exactly those and nothing else\n const cleared = keys ? had.filter((key) => keys.includes(key)) : [...had];\n clearDrafts(drafts, locale, keys);\n saveDrafts(cfg, drafts);\n return { locale, approved: cleared.length, keys: cleared };\n}\n\n// Undo is not a nicety here: the flag records who wrote the text, so restoring it restores a fact.\nexport function unapprove(cfg: ResolvedConfig, locale: string, keys: string[]): ApproveResult {\n if (!cfg.locales.includes(locale)) {\n throw badRequest(`${locale} is not one of this project's locales`);\n }\n const drafts = loadDrafts(cfg);\n markDrafts(drafts, locale, keys);\n saveDrafts(cfg, drafts);\n return { locale, approved: keys.length, keys };\n}\n","import { badRequest, HttpError } from './http';\n\nexport type JobState = 'running' | 'done' | 'failed';\n\nexport interface Job {\n id: string;\n kind: 'translate';\n state: JobState;\n total: number;\n done: number;\n locale?: string;\n message?: string;\n result?: unknown;\n written?: number;\n kept?: number;\n}\n\n// One at a time, because two runs over the same catalogs is a race over the same files.\nlet current: Job | null = null;\nlet counter = 0;\n\nexport function running(): Job | null {\n return current?.state === 'running' ? current : null;\n}\n\nexport function read(id: string): Job {\n if (!current || current.id !== id) throw new HttpError(404, `no job called \"${id}\"`);\n return current;\n}\n\nexport function start(kind: Job['kind'], total: number): Job {\n if (running()) throw badRequest('a job is already running, wait for it or read its progress');\n counter += 1;\n current = { id: `${kind}-${counter}`, kind, state: 'running', total, done: 0 };\n return current;\n}\n\n// A batch that failed still finishes: the bar reports work attempted, and message says what broke.\nexport function advance(id: string, keys: number, locale: string, error?: string): void {\n if (current?.id !== id) return;\n current.done = Math.min(current.done + keys, current.total);\n current.locale = locale;\n if (error) current.message = error;\n}\n\n// A run that writes part of its work and then fails is a success for what it wrote, so both land.\nexport function finish(id: string, state: 'done' | 'failed', patch: Partial<Job> = {}): void {\n if (current?.id === id) Object.assign(current, patch, { state });\n}\n","import {\n collectOrigins,\n counted,\n extractProject,\n loadCatalogs,\n loadState,\n mergeTranslations,\n recordTranslations,\n resolveProvider,\n shippedCatalogs,\n syncProject,\n translateCatalogs,\n} from '@verbaly/compiler';\nimport type { ResolvedConfig, TranslationWrite } from '@verbaly/compiler';\nimport { badRequest } from './http';\nimport { advance, finish, start, type Job } from './jobs';\n\nexport interface ExtractResult {\n added: Record<string, string[]>;\n // only the source locale counts as new text: a target gains a blank for every gap it had\n found: number;\n // source texts the code wrote over, a hand edit in the catalog among them\n replaced: number;\n messages: number;\n // the sidecar could not be read or written: the catalogs were, its drafts and stamps were not\n stateProblem?: string;\n}\n\n// Local, fast and free, so it answers in the same request instead of becoming a job.\nexport async function runExtract(cfg: ResolvedConfig): Promise<ExtractResult> {\n if (!cfg.include.length) {\n throw badRequest('source scanning is off (include: []), so there is nothing to extract');\n }\n // the same path as the CLI: catalogs, types and the review state move together\n const result = await syncProject(cfg);\n return {\n added: result.added,\n found: result.added[cfg.sourceLocale]?.length ?? 0,\n replaced: result.replaced.length,\n messages: result.registry.messages().size,\n ...(result.stateProblem && { stateProblem: result.stateProblem }),\n };\n}\n\nexport interface Plan {\n pending: Record<string, string[]>;\n total: number;\n}\n\n// translate is the one command that spends money and minutes, so the panel always shows the bill.\nexport async function planTranslate(cfg: ResolvedConfig, locales?: string[]): Promise<Plan> {\n const provider = await resolveProvider(cfg);\n const catalogs = loadCatalogs(cfg);\n const result = await translateCatalogs(cfg, catalogs, provider, { locales, dryRun: true });\n const total = Object.values(result.pending).reduce((n, keys) => n + keys.length, 0);\n return { pending: result.pending, total };\n}\n\nexport async function startTranslate(cfg: ResolvedConfig, locales?: string[]): Promise<Job> {\n // a machine translation that lost its draft flag passes as reviewed: read the state first\n loadState(cfg);\n const provider = await resolveProvider(cfg);\n // the job carries its own denominator, so progress reads the same whether or not you asked first\n const plan = await planTranslate(cfg, locales);\n const job = start('translate', plan.total);\n const id = job.id;\n\n // the run outlives this request on purpose: the client asks the job how it is going\n void (async () => {\n try {\n const registry = await extractProject(cfg);\n // the provider reads the text that ships, which is the text its answer has to match\n const catalogs = shippedCatalogs(cfg, loadCatalogs(cfg), registry, { newKeys: false });\n const result = await translateCatalogs(cfg, catalogs, provider, {\n locales,\n batchSize: cfg.translate.batchSize,\n concurrency: cfg.translate.concurrency,\n retries: cfg.translate.retries,\n origins: await collectOrigins(cfg, registry),\n onProgress: (p) => advance(id, p.keys, p.locale, p.error),\n });\n // what a machine wrote stays a draft: the panel does not get to change that rule\n const writes: TranslationWrite[] = [];\n let written = 0;\n let kept = 0;\n for (const [locale, keys] of Object.entries(result.translated)) {\n const catalog = catalogs[locale] ?? {};\n const landed = mergeTranslations(cfg, locale, catalog, keys);\n writes.push({\n locale,\n draft: true,\n entries: landed.map((key) => ({ key, text: catalog[key]! })),\n });\n written += landed.length;\n kept += keys.length - landed.length;\n }\n recordTranslations(cfg, catalogs[cfg.sourceLocale] ?? {}, writes);\n // on a failure the panel shows only this line, so it says what landed and what did not\n const failed = result.failed[0];\n finish(id, failed ? 'failed' : 'done', {\n result,\n written,\n kept,\n message: failed\n ? `${counted(written, 'message')} translated, but ${counted(result.failed.length, 'batch', 'batches')} did not answer (${failed.error}): run it again for the rest`\n : undefined,\n });\n } catch (error) {\n finish(id, 'failed', { message: error instanceof Error ? error.message : String(error) });\n }\n })();\n\n return job;\n}\n","import { execFile, spawn } from 'node:child_process';\nimport { promisify } from 'node:util';\nimport { join, relative } from 'node:path';\nimport { parseCatalog, type Catalog, type ResolvedConfig } from '@verbaly/compiler';\n\nconst run = promisify(execFile);\n\nexport interface Commit {\n sha: string;\n author: string;\n date: string;\n subject: string;\n}\n\n// one field separator that no subject can contain, written as the escape and never as the byte\nconst FIELD = '\\u001f';\nconst FORMAT = `--format=%H${FIELD}%an${FIELD}%aI${FIELD}%s`;\n\n// the full sha is what git resolves without ambiguity, and seven characters is what a card shows\nexport function parseFullLog(stdout: string): Commit[] {\n const commits: Commit[] = [];\n for (const line of stdout.split('\\n')) {\n const [sha, author, date, ...rest] = line.split(FIELD);\n // a subject holds anything but a newline, so it is the last field and is rejoined\n if (!sha || !author || !date || !rest.length) continue;\n commits.push({ sha, author, date, subject: rest.join(FIELD) });\n }\n return commits;\n}\n\n// split out so the shape of a log line is tested without a repository and without writing one\nexport function parseLog(stdout: string): Commit[] {\n return parseFullLog(stdout).map((commit) => ({ ...commit, sha: commit.sha.slice(0, 7) }));\n}\n\n// git is asked, never assumed: no repository and no git both mean there is no history to show\nexport async function catalogHistory(cfg: ResolvedConfig, limit = 5): Promise<Commit[]> {\n const dir = relative(cfg.root, cfg.dir).replaceAll('\\\\', '/') || '.';\n try {\n const { stdout } = await run('git', ['log', `-n${limit}`, FORMAT, '--', dir], {\n cwd: cfg.root,\n timeout: 3000,\n windowsHide: true,\n });\n return parseLog(stdout);\n } catch {\n return [];\n }\n}\n\nexport interface Revision {\n commit: Commit;\n now: Catalog | undefined;\n before: Catalog | undefined;\n}\n\n// blame measured 26% wrong on verbaly-web: a sibling's comma rewrites a line, a value does not\nexport function attribute(\n revisions: Revision[],\n owners = new Map<string, Commit>(),\n): Map<string, Commit> {\n for (const { commit, now, before } of revisions) {\n if (!now || !before) continue;\n for (const key of new Set([...Object.keys(now), ...Object.keys(before)])) {\n if (!owners.has(key) && now[key] !== before[key]) owners.set(key, commit);\n }\n }\n return owners;\n}\n\n// a key can move in more than one catalog, so the answer is the newest of the per-file answers\nexport function mergeOwners(maps: Map<string, Commit>[]): Map<string, Commit> {\n const merged = new Map<string, Commit>();\n for (const map of maps) {\n for (const [key, commit] of map) {\n const held = merged.get(key);\n if (!held || Date.parse(commit.date) > Date.parse(held.date)) merged.set(key, commit);\n }\n }\n return merged;\n}\n\nconst BLOB = /^[0-9a-f]{40} \\w+ (\\d+)$/;\n\n// git cat-file --batch answers every revision in one process: a header line, then that many bytes\nfunction readBlobs(\n root: string,\n requests: string[],\n take: (index: number, body: string | null, blob: string) => void,\n): Promise<boolean> {\n return new Promise((resolve) => {\n const child = spawn('git', ['cat-file', '--batch'], { cwd: root, windowsHide: true });\n // a killed batch leaves a half map, and a half map answers \"never committed\" for the rest\n const stop = setTimeout(() => {\n ok = false;\n child.kill();\n }, 20000);\n let pending: Buffer = Buffer.alloc(0);\n let index = 0;\n let ok = true;\n const fail = () => {\n ok = false;\n };\n child.on('error', fail);\n child.stdin.on('error', fail);\n child.stdout.on('data', (chunk: Buffer) => {\n pending = pending.length ? Buffer.concat([pending, chunk]) : chunk;\n for (;;) {\n const end = pending.indexOf(10);\n if (end < 0) break;\n const header = pending.toString('utf8', 0, end);\n const blob = BLOB.exec(header);\n // anything but a blob header is a revision git could not resolve, which is an answer too\n if (!blob) {\n take(index++, null, '');\n pending = pending.subarray(end + 1);\n continue;\n }\n const size = Number(blob[1]);\n if (pending.length < end + size + 2) break;\n take(index++, pending.toString('utf8', end + 1, end + 1 + size), header.slice(0, 40));\n pending = pending.subarray(end + size + 2);\n }\n });\n child.on('close', () => {\n clearTimeout(stop);\n resolve(ok);\n });\n child.stdin.end(requests.join('\\n') + '\\n');\n });\n}\n\nexport interface KeyHistory {\n owners: Map<string, Commit>;\n committed: Set<string>;\n ok: boolean;\n}\n\n// the window a local tool is willing to read: past this, the answer is \"older than what I read\"\nconst WINDOW = 200;\n\nasync function walkFile(root: string, file: string, limit: number) {\n const { stdout } = await run('git', ['log', `-n${limit}`, FORMAT, '--', file], {\n cwd: root,\n timeout: 5000,\n windowsHide: true,\n maxBuffer: 8_000_000,\n });\n const commits = parseFullLog(stdout);\n if (!commits.length) return { owners: new Map<string, Commit>(), head: [] as string[] };\n\n const owners = new Map<string, Commit>();\n let head: string[] = [];\n let waiting: Catalog | undefined;\n // only two flat catalogs are ever live, and the pair shares a blob, so one entry caches it\n let last: { blob: string; flat: Catalog | undefined } | null = null;\n const parse = (body: string | null, blob: string): Catalog | undefined => {\n if (body === null) return {};\n if (last && last.blob === blob) return last.flat;\n const flat = parseCatalog(body);\n last = { blob, flat };\n return flat;\n };\n\n const whole = await readBlobs(\n root,\n commits.flatMap((commit) => [`${commit.sha}:${file}`, `${commit.sha}^:${file}`]),\n (index, body, blob) => {\n const flat = parse(body, blob);\n if (index % 2 === 0) {\n waiting = flat;\n if (index === 0 && flat) head = Object.keys(flat);\n return;\n }\n const commit = commits[(index - 1) / 2];\n if (commit) attribute([{ commit, now: waiting, before: flat }], owners);\n waiting = undefined;\n },\n );\n if (!whole) throw new Error('[verbaly] git could not read every revision of this catalog');\n return { owners, head };\n}\n\n// The whole map at once, because a per-key query costs a git process and every row has a menu.\nexport async function catalogKeyHistory(\n cfg: ResolvedConfig,\n limit = WINDOW,\n): Promise<KeyHistory> {\n const maps: Map<string, Commit>[] = [];\n const committed = new Set<string>();\n let ok = false;\n for (const locale of cfg.locales) {\n const file = relative(cfg.root, join(cfg.dir, `${locale}.json`)).replaceAll('\\\\', '/');\n try {\n const walked = await walkFile(cfg.root, file, limit);\n maps.push(walked.owners);\n for (const key of walked.head) committed.add(key);\n ok = true;\n } catch {\n // one unreadable locale is not the end of the walk: the others still answer for their keys\n }\n }\n return { owners: mergeOwners(maps), committed, ok };\n}\n\nexport interface KeyCommit {\n key: string;\n commit: Commit | null;\n reason?: 'nogit' | 'uncommitted' | 'older';\n}\n\nconst walked = new Map<string, { head: string; history: KeyHistory }>();\n\nasync function headSha(root: string): Promise<string> {\n try {\n const { stdout } = await run('git', ['rev-parse', 'HEAD'], {\n cwd: root,\n timeout: 3000,\n windowsHide: true,\n });\n return stdout.trim();\n } catch {\n return '';\n }\n}\n\n// HEAD moving is the only thing that can change an answer, so it is what invalidates the walk\nasync function keyHistory(cfg: ResolvedConfig): Promise<KeyHistory> {\n const head = await headSha(cfg.root);\n const held = walked.get(cfg.root);\n if (held && held.head === head) return held.history;\n const history = await catalogKeyHistory(cfg);\n walked.set(cfg.root, { head, history });\n return history;\n}\n\nexport async function keyCommit(cfg: ResolvedConfig, key: string): Promise<KeyCommit> {\n const history = await keyHistory(cfg);\n if (!history.ok) return { key, commit: null, reason: 'nogit' };\n const commit = history.owners.get(key);\n // seven characters is what a person reads and pastes, and the walk needed the full one\n if (commit) return { key, commit: { ...commit, sha: commit.sha.slice(0, 7) } };\n return { key, commit: null, reason: history.committed.has(key) ? 'older' : 'uncommitted' };\n}\n","import { existsSync, readFileSync, statSync } from 'node:fs';\nimport { join, normalize, resolve } from 'node:path';\nimport { fileURLToPath } from 'node:url';\n\n// the built panel sits beside the compiled server, so it is found relative to this file, never cwd\nconst UI = resolve(fileURLToPath(new URL('.', import.meta.url)), 'ui');\n\nconst TYPES: Record<string, string> = {\n '.html': 'text/html; charset=utf-8',\n '.js': 'text/javascript; charset=utf-8',\n '.css': 'text/css; charset=utf-8',\n '.svg': 'image/svg+xml',\n '.woff2': 'font/woff2',\n '.json': 'application/json; charset=utf-8',\n '.ico': 'image/x-icon',\n};\n\nexport interface Asset {\n body: Buffer;\n type: string;\n}\n\nexport const uiBuilt = (): boolean => existsSync(join(UI, 'index.html'));\n\n// A path is resolved and then checked to still be inside UI: ../ and an absolute one both land out.\nexport function readAsset(pathname: string): Asset | undefined {\n const wanted = pathname === '/' ? '/index.html' : pathname;\n const target = resolve(join(UI, normalize(wanted)));\n if (target !== UI && !target.startsWith(UI + (process.platform === 'win32' ? '\\\\' : '/'))) {\n return undefined;\n }\n if (!existsSync(target) || !statSync(target).isFile()) return undefined;\n const dot = target.lastIndexOf('.');\n return {\n body: readFileSync(target),\n type: TYPES[target.slice(dot).toLowerCase()] ?? 'application/octet-stream',\n };\n}\n","import { randomBytes, timingSafeEqual } from 'node:crypto';\n\nexport interface GuardOptions {\n port: number;\n token: string;\n}\n\n// A local server that writes files is reachable from any page the browser has open.\nexport function newToken(): string {\n return randomBytes(9).toString('base64url');\n}\n\n// Only the two names that resolve to this machine. A DNS rebind arrives with a foreign Host.\nexport function hostAllowed(host: string | undefined, port: number): boolean {\n // a browser omits the port when it is the scheme default, so :80 would 403 every real request\n const names = port === 80 ? ['127.0.0.1', 'localhost'] : [];\n return [`127.0.0.1:${port}`, `localhost:${port}`, ...names].includes(host ?? '');\n}\n\n// No Origin at all is a same-origin navigation or a curl, both fine; a foreign one never is.\nexport function originAllowed(origin: string | undefined, port: number): boolean {\n if (origin === undefined) return true;\n return origin === `http://127.0.0.1:${port}` || origin === `http://localhost:${port}`;\n}\n\n// Compared in constant time so the token cannot be guessed one character at a time.\nexport function tokenMatches(given: string | null, expected: string): boolean {\n if (given === null) return false;\n const a = Buffer.from(given);\n const b = Buffer.from(expected);\n return a.length === b.length && timingSafeEqual(a, b);\n}\n\nexport interface GuardVerdict {\n ok: boolean;\n status: number;\n reason: string;\n}\n\nconst PASS: GuardVerdict = { ok: true, status: 200, reason: '' };\n\nexport function guardRequest(\n headers: { host?: string; origin?: string },\n url: URL,\n { port, token }: GuardOptions,\n): GuardVerdict {\n if (!hostAllowed(headers.host, port)) {\n return { ok: false, status: 403, reason: '[verbaly] unexpected Host header' };\n }\n if (!originAllowed(headers.origin, port)) {\n return { ok: false, status: 403, reason: '[verbaly] cross-origin request' };\n }\n if (!tokenMatches(url.searchParams.get('t'), token)) {\n return { ok: false, status: 401, reason: '[verbaly] missing or wrong token, open the printed url' };\n }\n return PASS;\n}\n","import { createServer, type IncomingMessage, type Server, type ServerResponse } from 'node:http';\nimport type { AddressInfo } from 'node:net';\nimport type { ResolvedConfig } from '@verbaly/compiler';\nimport { approve, buildState, health, unapprove, writeMessage } from './api';\nimport { planTranslate, runExtract, startTranslate } from './actions';\nimport { read as readJob, running } from './jobs';\nimport { catalogHistory, keyCommit } from './history';\nimport { readAsset, uiBuilt } from './assets';\nimport { guardRequest, newToken } from './guard';\nimport { HttpError, badRequest, scrub } from './http';\n\nexport interface StudioServer {\n server: Server;\n port: number;\n token: string;\n url: string;\n close: () => Promise<void>;\n}\n\nexport interface StartOptions {\n port?: number;\n token?: string;\n}\n\nconst DEFAULT_PORT = 4747;\nconst MAX_BODY = 1_000_000;\n\nfunction send(res: ServerResponse, status: number, body: unknown): void {\n res.writeHead(status, {\n 'content-type': 'application/json; charset=utf-8',\n 'cache-control': 'no-store',\n });\n res.end(JSON.stringify(body));\n}\n\nasync function readJson(req: IncomingMessage): Promise<Record<string, unknown>> {\n const chunks: Buffer[] = [];\n let size = 0;\n for await (const chunk of req) {\n size += (chunk as Buffer).length;\n if (size > MAX_BODY) throw new HttpError(413, 'request body too large');\n chunks.push(chunk as Buffer);\n }\n if (!chunks.length) return {};\n let parsed: unknown;\n try {\n parsed = JSON.parse(Buffer.concat(chunks).toString('utf8'));\n } catch {\n throw badRequest('body is not valid JSON');\n }\n // null and an array both pass typeof 'object', and reading .text off them is a 500\n if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {\n throw badRequest('body must be a JSON object');\n }\n return parsed as Record<string, unknown>;\n}\n\nfunction segment(raw: string | undefined, what: string): string {\n let value: string;\n try {\n value = decodeURIComponent(raw ?? '');\n } catch {\n throw badRequest(`the ${what} in the url is not valid percent-encoding`);\n }\n if (!value) throw badRequest(`the url is missing a ${what}`);\n return value;\n}\n\n// One page, JSON on every route: a framework would only add a dependency to a local tool.\nexport function createStudioApp(cfg: ResolvedConfig, port: number, token: string) {\n return async function handle(req: IncomingMessage, res: ServerResponse): Promise<void> {\n // The url is built inside the try: `GET //` throws, and out here that would kill the process.\n try {\n let url: URL;\n try {\n url = new URL(req.url ?? '/', `http://127.0.0.1:${port}`);\n } catch {\n throw badRequest('the request target is not a valid url');\n }\n\n const api = url.pathname.startsWith('/api/');\n const verdict = guardRequest({ host: req.headers.host, origin: req.headers.origin }, url, {\n port,\n token,\n });\n // the panel files are the published npm package: only the project data needs a token\n if (!verdict.ok && (api || verdict.status !== 401)) {\n send(res, verdict.status, { error: verdict.reason });\n return;\n }\n\n if (!api && req.method === 'GET') {\n const asset = readAsset(url.pathname);\n if (asset) {\n res.writeHead(200, { 'content-type': asset.type, 'cache-control': 'no-store' });\n res.end(asset.body);\n return;\n }\n if (url.pathname === '/' && !uiBuilt()) {\n throw new HttpError(500, 'the panel is not in this install, so only /api/* answers here');\n }\n }\n\n if (req.method === 'GET' && url.pathname === '/api/state') {\n send(res, 200, await buildState(cfg));\n return;\n }\n if (req.method === 'GET' && url.pathname === '/api/history') {\n send(res, 200, { commits: await catalogHistory(cfg) });\n return;\n }\n if (req.method === 'GET' && url.pathname.startsWith('/api/commit/')) {\n const key = segment(url.pathname.slice('/api/commit/'.length), 'key');\n send(res, 200, await keyCommit(cfg, key));\n return;\n }\n if (req.method === 'GET' && url.pathname === '/api/health') {\n send(res, 200, await health(cfg));\n return;\n }\n if (req.method === 'PUT' && url.pathname.startsWith('/api/message/')) {\n const rest = url.pathname.slice('/api/message/'.length).split('/');\n const locale = segment(rest[0], 'locale');\n const key = segment(rest.slice(1).join('/'), 'key');\n const body = await readJson(req);\n if (typeof body.text !== 'string') throw badRequest('body needs a \"text\" string');\n send(res, 200, await writeMessage(cfg, locale, key, body.text));\n return;\n }\n if (req.method === 'POST' && url.pathname === '/api/approve') {\n const body = await readJson(req);\n if (typeof body.locale !== 'string') throw badRequest('body needs a \"locale\" string');\n const keys = Array.isArray(body.keys)\n ? body.keys.filter((k): k is string => typeof k === 'string')\n : undefined;\n if (body.undo === true) {\n if (!keys?.length) throw badRequest('undoing needs the \"keys\" it should put back');\n send(res, 200, unapprove(cfg, body.locale, keys));\n return;\n }\n send(res, 200, approve(cfg, body.locale, keys));\n return;\n }\n if (req.method === 'POST' && url.pathname === '/api/extract') {\n send(res, 200, await runExtract(cfg));\n return;\n }\n if (req.method === 'GET' && url.pathname === '/api/translate') {\n const locales = url.searchParams.getAll('locale');\n send(res, 200, await planTranslate(cfg, locales.length ? locales : undefined));\n return;\n }\n if (req.method === 'POST' && url.pathname === '/api/translate') {\n const body = await readJson(req);\n const locales = Array.isArray(body.locales)\n ? body.locales.filter((l): l is string => typeof l === 'string')\n : undefined;\n send(res, 200, await startTranslate(cfg, locales?.length ? locales : undefined));\n return;\n }\n // a reload loses the id, so the panel asks what is running rather than losing the bar\n if (req.method === 'GET' && url.pathname === '/api/job') {\n send(res, 200, running());\n return;\n }\n if (req.method === 'GET' && url.pathname.startsWith('/api/job/')) {\n send(res, 200, readJob(segment(url.pathname.slice('/api/job/'.length), 'job id')));\n return;\n }\n\n send(res, 404, { error: `[verbaly] no route for ${req.method} ${url.pathname}` });\n } catch (error) {\n const status = error instanceof HttpError ? error.status : 500;\n const message = error instanceof Error ? error.message : String(error);\n send(res, status, { error: scrub(message, cfg.root) });\n }\n };\n}\n\n// A busy port climbs instead of failing: this is a local tool, not a service on a fixed address.\nfunction listen(server: Server, port: number, attemptsLeft: number): Promise<number> {\n return new Promise((resolve, reject) => {\n const onError = (error: NodeJS.ErrnoException) => {\n if (error.code === 'EADDRINUSE' && attemptsLeft > 0) {\n server.removeListener('error', onError);\n resolve(listen(server, port + 1, attemptsLeft - 1));\n return;\n }\n reject(error);\n };\n server.once('error', onError);\n server.listen(port, '127.0.0.1', () => {\n server.removeListener('error', onError);\n // port 0 asks the system to pick, so the bound address is the only place the answer is\n const bound = server.address() as AddressInfo | string | null;\n resolve(typeof bound === 'object' && bound !== null ? bound.port : port);\n });\n });\n}\n\nexport async function startStudio(\n cfg: ResolvedConfig,\n options: StartOptions = {},\n): Promise<StudioServer> {\n const token = options.token ?? newToken();\n const server = createServer();\n // The handler is attached before listen so no request can arrive without one.\n let port = options.port ?? DEFAULT_PORT;\n server.on('request', (req, res) => void createStudioApp(cfg, port, token)(req, res));\n port = await listen(server, port, 20);\n\n return {\n server,\n port,\n token,\n url: `http://127.0.0.1:${port}/?t=${token}`,\n close: () =>\n new Promise<void>((resolve, reject) =>\n server.close((error) => (error ? reject(error) : resolve())),\n ),\n };\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@verbaly/studio",
3
- "version": "0.66.0",
3
+ "version": "0.68.0",
4
4
  "description": "Local review server for Verbaly: read, edit and approve translations over the JSON files already in your repo. No account, no database.",
5
5
  "keywords": [
6
6
  "i18n",
@@ -43,7 +43,7 @@
43
43
  "access": "public"
44
44
  },
45
45
  "dependencies": {
46
- "@verbaly/compiler": "^0.66.0"
46
+ "@verbaly/compiler": "^0.68.0"
47
47
  },
48
48
  "devDependencies": {
49
49
  "@sveltejs/vite-plugin-svelte": "^7.3.1",
@@ -51,8 +51,8 @@
51
51
  "svelte": "^5.57.1",
52
52
  "svelte-check": "^4.7.6",
53
53
  "typescript": "npm:@typescript/typescript6@^6.0.2",
54
- "verbaly": "^0.66.0",
55
- "vite": "^8.3.2"
54
+ "verbaly": "^0.68.0",
55
+ "vite": "^8.3.3"
56
56
  },
57
57
  "scripts": {
58
58
  "build": "tsdown && vite build",