@verbaly/studio 0.63.0 → 0.65.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/README.md CHANGED
@@ -109,6 +109,10 @@ catalogs is a race over the same files.
109
109
  **What a machine writes stays a draft**, here as everywhere else. The panel does not get to change
110
110
  that rule, so a finished run leaves you a list to read, not a job marked done.
111
111
 
112
+ **A run writes onto the catalog as it is when the run ends.** It takes minutes, and you can keep
113
+ fixing messages while it is out: anything you saved in the meantime keeps your text, never becomes
114
+ a draft, and the panel says how many it left alone.
115
+
112
116
  **`GET /api/commit/:key` answers about values, not about lines.** It walks your catalogs' history
113
117
  once and remembers, per message, the newest commit where that message's text really differs from its
114
118
  parent's. `git blame` is the obvious way to ask and it is wrong here: adding a key rewrites the
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, parseCatalog, readCatalog, resolveProvider, saveDrafts, status, syncCatalogs, targetLocales, translateCatalogs, validateMessage, validatePair, writeCatalog, writeDts } from "@verbaly/compiler";
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";
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";
@@ -171,11 +171,16 @@ function readDrafts(cfg) {
171
171
  }
172
172
  }
173
173
  function relativize(cfg, result) {
174
+ const rel = (file) => relative(cfg.root, file).replaceAll("\\", "/");
174
175
  return {
175
176
  ...result,
176
177
  unknown: result.unknown.map((entry) => ({
177
178
  ...entry,
178
- files: entry.files.map((file) => relative(cfg.root, file).replaceAll("\\", "/"))
179
+ files: entry.files.map(rel)
180
+ })),
181
+ extra: result.extra.map((entry) => ({
182
+ ...entry,
183
+ files: entry.files.map(rel)
179
184
  }))
180
185
  };
181
186
  }
@@ -341,12 +346,20 @@ async function startTranslate(cfg, locales) {
341
346
  origins: await collectOrigins(cfg),
342
347
  onProgress: (p) => advance(id, p.keys, p.locale, p.error)
343
348
  });
344
- for (const [locale, keys] of Object.entries(result.translated)) if (keys.length) writeCatalog(cfg, locale, catalogs[locale] ?? {});
345
349
  const drafts = loadDrafts(cfg);
346
- for (const [locale, keys] of Object.entries(result.translated)) markDrafts(drafts, locale, keys);
350
+ let written = 0;
351
+ let kept = 0;
352
+ for (const [locale, keys] of Object.entries(result.translated)) {
353
+ const landed = mergeTranslations(cfg, locale, catalogs[locale] ?? {}, keys);
354
+ markDrafts(drafts, locale, landed);
355
+ written += landed.length;
356
+ kept += keys.length - landed.length;
357
+ }
347
358
  saveDrafts(cfg, drafts);
348
359
  finish(id, result.failed.length ? "failed" : "done", {
349
360
  result,
361
+ written,
362
+ kept,
350
363
  message: result.failed.length ? `${result.failed.length} batches did not answer` : void 0
351
364
  });
352
365
  } catch (error) {
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 file a stray key was used in as an absolute path, origins already do not.\nfunction relativize(cfg: ResolvedConfig, result: CheckResult): CheckResult {\n return {\n ...result,\n unknown: result.unknown.map((entry) => ({\n ...entry,\n files: entry.files.map((file) => relative(cfg.root, file).replaceAll('\\\\', '/')),\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 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}\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 extractProject,\n loadCatalogs,\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, and cfg.dts false means the project opted out\n if (cfg.dts !== false) writeDts(cfg, catalogs[cfg.sourceLocale] ?? {}, cfg.dts);\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 for (const [locale, keys] of Object.entries(result.translated)) {\n if (keys.length) writeCatalog(cfg, locale, catalogs[locale] ?? {});\n }\n // what a machine wrote stays a draft: the panel does not get to change that rule\n const drafts = loadDrafts(cfg);\n for (const [locale, keys] of Object.entries(result.translated)) {\n markDrafts(drafts, locale, keys);\n }\n saveDrafts(cfg, drafts);\n finish(id, result.failed.length ? 'failed' : 'done', {\n result,\n message: result.failed.length ? `${result.failed.length} batches did not answer` : 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 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 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, and cfg.dts false means the project opted out\n if (cfg.dts !== false) writeDts(cfg, catalogs[cfg.sourceLocale] ?? {}, cfg.dts);\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 finish(id, result.failed.length ? 'failed' : 'done', {\n result,\n written,\n kept,\n message: result.failed.length ? `${result.failed.length} batches did not answer` : 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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"}
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, doctor, effectiveDrafts, extractProject, loadCatalogs, loadDrafts, markDrafts, parseCatalog, readCatalog, resolveProvider, saveDrafts, status, syncCatalogs, targetLocales, translateCatalogs, validateMessage, validatePair, writeCatalog, writeDts } from "@verbaly/compiler";
4
+ import { DRAFTS_FILE, check, clearDrafts, collectOrigins, doctor, effectiveDrafts, extractProject, loadCatalogs, loadDrafts, markDrafts, mergeTranslations, parseCatalog, readCatalog, resolveProvider, saveDrafts, status, syncCatalogs, targetLocales, translateCatalogs, validateMessage, validatePair, writeCatalog, writeDts } 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";
@@ -170,11 +170,16 @@ function readDrafts(cfg) {
170
170
  }
171
171
  }
172
172
  function relativize(cfg, result) {
173
+ const rel = (file) => relative(cfg.root, file).replaceAll("\\", "/");
173
174
  return {
174
175
  ...result,
175
176
  unknown: result.unknown.map((entry) => ({
176
177
  ...entry,
177
- files: entry.files.map((file) => relative(cfg.root, file).replaceAll("\\", "/"))
178
+ files: entry.files.map(rel)
179
+ })),
180
+ extra: result.extra.map((entry) => ({
181
+ ...entry,
182
+ files: entry.files.map(rel)
178
183
  }))
179
184
  };
180
185
  }
@@ -340,12 +345,20 @@ async function startTranslate(cfg, locales) {
340
345
  origins: await collectOrigins(cfg),
341
346
  onProgress: (p) => advance(id, p.keys, p.locale, p.error)
342
347
  });
343
- for (const [locale, keys] of Object.entries(result.translated)) if (keys.length) writeCatalog(cfg, locale, catalogs[locale] ?? {});
344
348
  const drafts = loadDrafts(cfg);
345
- for (const [locale, keys] of Object.entries(result.translated)) markDrafts(drafts, locale, keys);
349
+ let written = 0;
350
+ let kept = 0;
351
+ for (const [locale, keys] of Object.entries(result.translated)) {
352
+ const landed = mergeTranslations(cfg, locale, catalogs[locale] ?? {}, keys);
353
+ markDrafts(drafts, locale, landed);
354
+ written += landed.length;
355
+ kept += keys.length - landed.length;
356
+ }
346
357
  saveDrafts(cfg, drafts);
347
358
  finish(id, result.failed.length ? "failed" : "done", {
348
359
  result,
360
+ written,
361
+ kept,
349
362
  message: result.failed.length ? `${result.failed.length} batches did not answer` : void 0
350
363
  });
351
364
  } catch (error) {