@verbaly/compiler 0.26.0 → 0.27.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.
@@ -23,7 +23,9 @@ function claudeProvider(options = {}) {
23
23
  };
24
24
  }
25
25
  function buildPrompt(request) {
26
- return `Translate each value from "${request.sourceLocale}" to "${request.targetLocale}". Return a JSON object with the same keys and translated values.\n\n` + JSON.stringify(request.messages, null, 2);
26
+ const origins = request.origins;
27
+ const context = origins && Object.keys(origins).length ? `\n\nWhere each string appears (context for tone and length, do not translate these paths):\n` + Object.entries(origins).map(([key, files]) => ` ${key}: ${files.join(", ")}`).join("\n") : "";
28
+ return `Translate each value from "${request.sourceLocale}" to "${request.targetLocale}". Return a JSON object with the same keys and translated values.\n\n` + JSON.stringify(request.messages, null, 2) + context;
27
29
  }
28
30
  function batchFormat(request) {
29
31
  const keys = Object.keys(request.messages);
@@ -48,4 +50,4 @@ async function loadSdk(load = () => import("@anthropic-ai/sdk")) {
48
50
  //#endregion
49
51
  export { claudeProvider };
50
52
 
51
- //# sourceMappingURL=claude-C-ETlqsW.js.map
53
+ //# sourceMappingURL=claude-h9WA0ppg.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"claude-h9WA0ppg.js","names":[],"sources":["../src/providers/claude.ts"],"sourcesContent":["import { isModuleNotFound } from '../config';\nimport type { TranslateProvider, TranslateRequest } from '../translate';\n\nexport interface ClaudeProviderOptions {\n model?: string;\n apiKey?: string;\n maxTokens?: number;\n}\n\n// balanced default for translation\nconst DEFAULT_MODEL = 'claude-sonnet-5';\n\nconst SYSTEM = `You translate UI strings for the Verbaly i18n library.\nRules:\n- Translate only the human-readable text, naturally for the target locale.\n- Preserve verbatim: placeholders like {name}, format specs like {price:currency/EUR}, variant blocks like {count | one: ... | other: # ...} (translate only the text inside each variant, keep keys and # as-is), ICU syntax, named tags like <em>...</em> and escapes {{ }} || ##.\n- Keys are opaque identifiers: return exactly the same keys, never translate or rename them.`;\n\nexport function claudeProvider(options: ClaudeProviderOptions = {}): TranslateProvider {\n return async (request: TranslateRequest) => {\n const Anthropic = await loadSdk();\n const client = new Anthropic(options.apiKey ? { apiKey: options.apiKey } : {});\n const response = await client.messages.create({\n model: options.model ?? DEFAULT_MODEL,\n max_tokens: options.maxTokens ?? 16000,\n thinking: { type: 'disabled' },\n system: SYSTEM,\n messages: [{ role: 'user', content: buildPrompt(request) }],\n output_config: { format: batchFormat(request) },\n });\n const text = response.content.find((block) => block.type === 'text')?.text ?? '{}';\n return JSON.parse(text) as Record<string, string>;\n };\n}\n\nexport function buildPrompt(request: TranslateRequest): string {\n const origins = request.origins;\n const context =\n origins && Object.keys(origins).length\n ? `\\n\\nWhere each string appears (context for tone and length, do not translate these paths):\\n` +\n Object.entries(origins)\n .map(([key, files]) => ` ${key}: ${files.join(', ')}`)\n .join('\\n')\n : '';\n return (\n `Translate each value from \"${request.sourceLocale}\" to \"${request.targetLocale}\". ` +\n `Return a JSON object with the same keys and translated values.\\n\\n` +\n JSON.stringify(request.messages, null, 2) +\n context\n );\n}\n\nexport function batchFormat(request: TranslateRequest) {\n const keys = Object.keys(request.messages);\n return {\n type: 'json_schema' as const,\n schema: {\n type: 'object',\n properties: Object.fromEntries(keys.map((key) => [key, { type: 'string' }])),\n required: keys,\n additionalProperties: false,\n },\n };\n}\n\ntype SdkModule = { default: typeof import('@anthropic-ai/sdk').default };\n\n// injectable for tests\nexport async function loadSdk(\n load: () => Promise<SdkModule> = () => import('@anthropic-ai/sdk'),\n): Promise<SdkModule['default']> {\n try {\n const mod = await load();\n return mod.default;\n } catch (error) {\n if (isModuleNotFound(error, '@anthropic-ai/sdk')) {\n throw new Error(\n '[verbaly] the claude translate provider needs @anthropic-ai/sdk: install it as a dev dependency (e.g. `pnpm add -D @anthropic-ai/sdk` / `npm i -D @anthropic-ai/sdk`) and set ANTHROPIC_API_KEY',\n { cause: error },\n );\n }\n throw error;\n }\n}\n"],"mappings":";;AAUA,MAAM,gBAAgB;AAEtB,MAAM,SAAS;;;;;AAMf,SAAgB,eAAe,UAAiC,CAAC,GAAsB;CACrF,OAAO,OAAO,YAA8B;EAW1C,MAAM,QAAO,MARU,KADJ,OADK,QAAQ,IACH,QAAQ,SAAS,EAAE,QAAQ,QAAQ,OAAO,IAAI,CAAC,CAChD,CAAC,CAAC,SAAS,OAAO;GAC5C,OAAO,QAAQ,SAAS;GACxB,YAAY,QAAQ,aAAa;GACjC,UAAU,EAAE,MAAM,WAAW;GAC7B,QAAQ;GACR,UAAU,CAAC;IAAE,MAAM;IAAQ,SAAS,YAAY,OAAO;GAAE,CAAC;GAC1D,eAAe,EAAE,QAAQ,YAAY,OAAO,EAAE;EAChD,CAAC,EAAA,CACqB,QAAQ,MAAM,UAAU,MAAM,SAAS,MAAM,CAAC,EAAE,QAAQ;EAC9E,OAAO,KAAK,MAAM,IAAI;CACxB;AACF;AAEA,SAAgB,YAAY,SAAmC;CAC7D,MAAM,UAAU,QAAQ;CACxB,MAAM,UACJ,WAAW,OAAO,KAAK,OAAO,CAAC,CAAC,SAC5B,iGACA,OAAO,QAAQ,OAAO,CAAC,CACpB,KAAK,CAAC,KAAK,WAAW,KAAK,IAAI,IAAI,MAAM,KAAK,IAAI,GAAG,CAAC,CACtD,KAAK,IAAI,IACZ;CACN,OACE,8BAA8B,QAAQ,aAAa,QAAQ,QAAQ,aAAa,yEAEhF,KAAK,UAAU,QAAQ,UAAU,MAAM,CAAC,IACxC;AAEJ;AAEA,SAAgB,YAAY,SAA2B;CACrD,MAAM,OAAO,OAAO,KAAK,QAAQ,QAAQ;CACzC,OAAO;EACL,MAAM;EACN,QAAQ;GACN,MAAM;GACN,YAAY,OAAO,YAAY,KAAK,KAAK,QAAQ,CAAC,KAAK,EAAE,MAAM,SAAS,CAAC,CAAC,CAAC;GAC3E,UAAU;GACV,sBAAsB;EACxB;CACF;AACF;AAKA,eAAsB,QACpB,aAAuC,OAAO,sBACf;CAC/B,IAAI;EAEF,QAAO,MADW,KAAK,EAAA,CACZ;CACb,SAAS,OAAO;EACd,IAAI,iBAAiB,OAAO,mBAAmB,GAC7C,MAAM,IAAI,MACR,mMACA,EAAE,OAAO,MAAM,CACjB;EAEF,MAAM;CACR;AACF"}
package/dist/cli.js CHANGED
@@ -387,7 +387,7 @@ function resolveConfig(config = {}) {
387
387
  const sourceLocale = config.sourceLocale ?? "en";
388
388
  const locales = /* @__PURE__ */ new Set([sourceLocale, ...config.locales ?? []]);
389
389
  if (existsSync(dir)) {
390
- for (const file of readdirSync(dir)) if (file.endsWith(".json") && file !== `en-XA.json`) locales.add(file.slice(0, -5));
390
+ for (const file of readdirSync(dir)) if (file.endsWith(".json") && !file.startsWith(".") && file !== `en-XA.json`) locales.add(file.slice(0, -5));
391
391
  }
392
392
  return {
393
393
  root,
@@ -1083,6 +1083,66 @@ function readDeps(root) {
1083
1083
  }
1084
1084
  }
1085
1085
  //#endregion
1086
+ //#region src/drafts.ts
1087
+ const DRAFTS_FILE = ".verbaly-drafts.json";
1088
+ function draftsPath(cfg) {
1089
+ return join(cfg.dir, DRAFTS_FILE);
1090
+ }
1091
+ function loadDrafts(cfg) {
1092
+ let content;
1093
+ try {
1094
+ content = readFileSync(draftsPath(cfg), "utf8");
1095
+ } catch {
1096
+ return {};
1097
+ }
1098
+ try {
1099
+ return JSON.parse(content.replace(/^\uFEFF/, ""));
1100
+ } catch (error) {
1101
+ throw new Error(`[verbaly] ${draftsPath(cfg)} is not valid JSON, fix or delete the file`, { cause: error });
1102
+ }
1103
+ }
1104
+ function serializeDrafts(drafts) {
1105
+ const sorted = {};
1106
+ for (const locale of Object.keys(drafts).sort()) {
1107
+ const keys = [...new Set(drafts[locale])].sort();
1108
+ if (keys.length) sorted[locale] = keys;
1109
+ }
1110
+ return JSON.stringify(sorted, null, 2) + "\n";
1111
+ }
1112
+ function saveDrafts(cfg, drafts) {
1113
+ const serialized = serializeDrafts(drafts);
1114
+ const path = draftsPath(cfg);
1115
+ try {
1116
+ if (readFileSync(path, "utf8") === serialized) return;
1117
+ } catch {
1118
+ mkdirSync(cfg.dir, { recursive: true });
1119
+ }
1120
+ writeFileSync(path, serialized);
1121
+ }
1122
+ function markDrafts(drafts, locale, keys) {
1123
+ if (!keys.length) return;
1124
+ drafts[locale] = [.../* @__PURE__ */ new Set([...drafts[locale] ?? [], ...keys])];
1125
+ }
1126
+ function clearDrafts(drafts, locale, keys) {
1127
+ if (!drafts[locale]) return;
1128
+ if (!keys) {
1129
+ delete drafts[locale];
1130
+ return;
1131
+ }
1132
+ const drop = new Set(keys);
1133
+ drafts[locale] = drafts[locale].filter((key) => !drop.has(key));
1134
+ if (!drafts[locale].length) delete drafts[locale];
1135
+ }
1136
+ function effectiveDrafts(drafts, catalogs) {
1137
+ const out = {};
1138
+ for (const [locale, keys] of Object.entries(drafts)) {
1139
+ const catalog = catalogs[locale] ?? {};
1140
+ const live = keys.filter((key) => catalog[key]);
1141
+ if (live.length) out[locale] = live;
1142
+ }
1143
+ return out;
1144
+ }
1145
+ //#endregion
1086
1146
  //#region src/mobile.ts
1087
1147
  function androidValuesDir(locale, sourceLocale) {
1088
1148
  if (locale === sourceLocale) return "values";
@@ -1143,10 +1203,12 @@ async function translateCatalogs(cfg, catalogs, provider, options = {}) {
1143
1203
  for (let i = 0; i < missing.length; i += batchSize) {
1144
1204
  const keys = missing.slice(i, i + batchSize);
1145
1205
  const messages = Object.fromEntries(keys.map((key) => [key, source[key]]));
1206
+ const origins = options.origins ? Object.fromEntries(keys.filter((key) => options.origins[key]).map((key) => [key, options.origins[key]])) : void 0;
1146
1207
  const out = await provider({
1147
1208
  sourceLocale: cfg.sourceLocale,
1148
1209
  targetLocale: locale,
1149
- messages
1210
+ messages,
1211
+ origins
1150
1212
  });
1151
1213
  for (const key of keys) {
1152
1214
  const text = out[key];
@@ -1670,9 +1732,10 @@ function decodeEntities(text) {
1670
1732
  }
1671
1733
  //#endregion
1672
1734
  //#region src/status.ts
1673
- function status(cfg, catalogs, registry) {
1735
+ function status(cfg, catalogs, registry, drafts = {}) {
1674
1736
  const source = catalogs[cfg.sourceLocale] ?? {};
1675
1737
  const needed = /* @__PURE__ */ new Set([...registry.messages().keys(), ...Object.keys(source)]);
1738
+ const live = effectiveDrafts(drafts, catalogs);
1676
1739
  const locales = [];
1677
1740
  for (const locale of cfg.locales) {
1678
1741
  if (locale === cfg.sourceLocale) continue;
@@ -1681,7 +1744,8 @@ function status(cfg, catalogs, registry) {
1681
1744
  locales.push({
1682
1745
  locale,
1683
1746
  translated,
1684
- total: needed.size
1747
+ total: needed.size,
1748
+ drafts: live[locale]?.length ?? 0
1685
1749
  });
1686
1750
  }
1687
1751
  return {
@@ -1696,10 +1760,11 @@ function formatStatusResult(result) {
1696
1760
  lines.push(" no target locales (add locales to your config)");
1697
1761
  return lines.join("\n");
1698
1762
  }
1699
- for (const { locale, translated, total } of result.locales) {
1763
+ for (const { locale, translated, total, drafts } of result.locales) {
1700
1764
  const pct = total === 0 ? 100 : Math.floor(translated / total * 100);
1701
1765
  const mark = translated === total ? " ✓" : "";
1702
- lines.push(` ${locale}: ${translated}/${total} translated (${pct}%)${mark}`);
1766
+ const draftNote = drafts > 0 ? `, ${drafts} unreviewed` : "";
1767
+ lines.push(` ${locale}: ${translated}/${total} translated (${pct}%${draftNote})${mark}`);
1703
1768
  }
1704
1769
  return lines.join("\n");
1705
1770
  }
@@ -1969,6 +2034,7 @@ Usage:
1969
2034
  verbaly status translation coverage per locale, at a glance
1970
2035
  verbaly check verify translations are complete (CI)
1971
2036
  verbaly translate fill missing translations via a provider (default: claude)
2037
+ verbaly review list machine translations awaiting review (--approve marks them reviewed)
1972
2038
  verbaly export write translator files (XLIFF 2.0, CSV) or mobile resources (Android, iOS)
1973
2039
  verbaly import <files…> fill catalogs back from translated XLIFF/CSV files
1974
2040
  verbaly pseudo generate a pseudo-locale catalog for i18n QA (default: en-XA)
@@ -1983,6 +2049,8 @@ Options:
1983
2049
  --watch keep extracting as source files change (extract)
1984
2050
  --write apply the rewrites instead of only reporting (wrap)
1985
2051
  --json machine-readable output (status)
2052
+ --drafts also fail on unreviewed machine translations (check)
2053
+ --approve mark listed drafts as reviewed (review)
1986
2054
  --reporter <name> failure format: text (default) or github annotations (check)
1987
2055
  --model <id> model override for the claude provider (translate)
1988
2056
  --dry-run list what would happen, write nothing (translate, import, extract)
@@ -2013,6 +2081,8 @@ async function runCli(args = process.argv.slice(2)) {
2013
2081
  watch: { type: "boolean" },
2014
2082
  write: { type: "boolean" },
2015
2083
  json: { type: "boolean" },
2084
+ drafts: { type: "boolean" },
2085
+ approve: { type: "boolean" },
2016
2086
  reporter: { type: "string" },
2017
2087
  model: { type: "string" },
2018
2088
  locale: { type: "string" },
@@ -2131,7 +2201,7 @@ async function runCli(args = process.argv.slice(2)) {
2131
2201
  }
2132
2202
  if (command === "status") {
2133
2203
  const registry = await extractProject(cfg);
2134
- const result = status(cfg, loadCatalogs(cfg), registry);
2204
+ const result = status(cfg, loadCatalogs(cfg), registry, loadDrafts(cfg));
2135
2205
  console.log(values.json ? JSON.stringify(result) : formatStatusResult(result));
2136
2206
  return;
2137
2207
  }
@@ -2143,11 +2213,20 @@ async function runCli(args = process.argv.slice(2)) {
2143
2213
  return;
2144
2214
  }
2145
2215
  const registry = await extractProject(cfg);
2146
- const result = check(cfg, loadCatalogs(cfg), registry);
2147
- if (result.ok) {
2216
+ const catalogs = loadCatalogs(cfg);
2217
+ const result = check(cfg, catalogs, registry);
2218
+ const unreviewed = values.drafts ? effectiveDrafts(loadDrafts(cfg), catalogs) : {};
2219
+ const draftKeys = Object.entries(unreviewed);
2220
+ if (result.ok && draftKeys.length === 0) {
2148
2221
  console.log("[verbaly] all translations complete ✓");
2149
2222
  return;
2150
2223
  }
2224
+ if (result.ok && draftKeys.length > 0) {
2225
+ for (const [locale, keys] of draftKeys) console.error(` [${locale}] ${keys.length} unreviewed: ${keys.join(", ")}`);
2226
+ console.error("[verbaly] check failed: machine translations awaiting review (run verbaly review --approve)");
2227
+ process.exitCode = 1;
2228
+ return;
2229
+ }
2151
2230
  if (reporter === "github") {
2152
2231
  for (const line of githubCheckAnnotations(result, registry, cfg.root)) console.error(line);
2153
2232
  console.error(`[verbaly] check failed: ${result.missing.length} missing, ${result.unknown.length} unknown`);
@@ -2160,7 +2239,8 @@ async function runCli(args = process.argv.slice(2)) {
2160
2239
  const result = await translateCatalogs(cfg, catalogs, await resolveProvider(cfg, values.model), {
2161
2240
  locales: values.locales?.split(","),
2162
2241
  batchSize: cfg.translate.batchSize,
2163
- dryRun: values["dry-run"]
2242
+ dryRun: values["dry-run"],
2243
+ origins: values["dry-run"] ? void 0 : await collectOrigins(cfg)
2164
2244
  });
2165
2245
  if (values["dry-run"]) {
2166
2246
  const entries = Object.entries(result.pending);
@@ -2171,14 +2251,43 @@ async function runCli(args = process.argv.slice(2)) {
2171
2251
  for (const [locale, keys] of entries) console.log(` ${locale}: ${keys.length} missing: ${keys.join(", ")}`);
2172
2252
  return;
2173
2253
  }
2254
+ const drafts = loadDrafts(cfg);
2174
2255
  for (const locale of Object.keys(result.translated)) {
2175
2256
  writeCatalog(cfg, locale, catalogs[locale] ?? {});
2176
- console.log(` ${locale}: +${result.translated[locale].length} translated`);
2257
+ markDrafts(drafts, locale, result.translated[locale]);
2258
+ console.log(` ${locale}: +${result.translated[locale].length} translated (draft)`);
2177
2259
  }
2260
+ if (Object.keys(result.translated).length > 0) saveDrafts(cfg, drafts);
2178
2261
  for (const [locale, keys] of Object.entries(result.invalid)) console.warn(` ${locale}: ${keys.length} rejected (params/tags not preserved): ${keys.join(", ")}`);
2179
2262
  if (Object.keys(result.translated).length === 0 && Object.keys(result.invalid).length === 0) console.log("[verbaly] nothing to translate ✓");
2180
2263
  return;
2181
2264
  }
2265
+ if (command === "review") {
2266
+ const catalogs = loadCatalogs(cfg);
2267
+ const drafts = loadDrafts(cfg);
2268
+ const live = effectiveDrafts(drafts, catalogs);
2269
+ const targets = values.locale ? { [values.locale]: live[values.locale] ?? [] } : live;
2270
+ const entries = Object.entries(targets).filter(([, keys]) => keys.length);
2271
+ if (entries.length === 0) {
2272
+ console.log("[verbaly] no machine translations awaiting review ✓");
2273
+ return;
2274
+ }
2275
+ if (values.approve) {
2276
+ let count = 0;
2277
+ for (const [locale, keys] of entries) {
2278
+ clearDrafts(drafts, locale, keys);
2279
+ count += keys.length;
2280
+ console.log(` ${locale}: ${keys.length} approved`);
2281
+ }
2282
+ saveDrafts(cfg, drafts);
2283
+ console.log(`[verbaly] ${count} translations marked reviewed ✓`);
2284
+ return;
2285
+ }
2286
+ const total = entries.reduce((sum, [, keys]) => sum + keys.length, 0);
2287
+ console.log(`[verbaly] ${total} machine translations awaiting review (--approve to accept)`);
2288
+ for (const [locale, keys] of entries) console.log(` ${locale}: ${keys.join(", ")}`);
2289
+ return;
2290
+ }
2182
2291
  if (command === "export") {
2183
2292
  const format = values.format ?? "xliff";
2184
2293
  if (![
@@ -2225,11 +2334,18 @@ async function runCli(args = process.argv.slice(2)) {
2225
2334
  overwrite: values.overwrite,
2226
2335
  dryRun: values["dry-run"]
2227
2336
  });
2337
+ const drafts = loadDrafts(cfg);
2338
+ let draftsChanged = false;
2228
2339
  for (const [locale, keys] of Object.entries(result.imported)) {
2229
- if (!values["dry-run"]) writeCatalog(cfg, locale, catalogs[locale] ?? {});
2340
+ if (!values["dry-run"]) {
2341
+ writeCatalog(cfg, locale, catalogs[locale] ?? {});
2342
+ clearDrafts(drafts, locale, keys);
2343
+ draftsChanged = true;
2344
+ }
2230
2345
  const verb = values["dry-run"] ? "would import" : "imported";
2231
2346
  console.log(` ${locale}: +${keys.length} ${verb}`);
2232
2347
  }
2348
+ if (draftsChanged) saveDrafts(cfg, drafts);
2233
2349
  for (const [locale, keys] of Object.entries(result.skipped)) console.log(` ${locale}: ${keys.length} already translated, kept (use --overwrite to replace)`);
2234
2350
  for (const [locale, keys] of Object.entries(result.rejected)) console.warn(` ${locale}: ${keys.length} rejected (params/tags not preserved): ${keys.join(", ")}`);
2235
2351
  for (const [locale, keys] of Object.entries(result.unknown)) console.warn(` ${locale}: ${keys.length} unknown keys ignored (not in the source catalog): ${keys.join(", ")}`);
@@ -2277,8 +2393,9 @@ const COMMAND_FLAGS = {
2277
2393
  ],
2278
2394
  wrap: ["write"],
2279
2395
  status: ["json"],
2280
- check: ["reporter"],
2396
+ check: ["reporter", "drafts"],
2281
2397
  translate: ["model", "dry-run"],
2398
+ review: ["approve", "locale"],
2282
2399
  export: [
2283
2400
  "format",
2284
2401
  "out",
@@ -2317,7 +2434,7 @@ async function collectOrigins(cfg) {
2317
2434
  async function resolveProvider(cfg, model) {
2318
2435
  const configured = cfg.translate.provider;
2319
2436
  if (typeof configured === "function") return configured;
2320
- const { claudeProvider } = await import("./claude-C-ETlqsW.js");
2437
+ const { claudeProvider } = await import("./claude-h9WA0ppg.js");
2321
2438
  return claudeProvider({ model: model ?? cfg.translate.model });
2322
2439
  }
2323
2440
  //#endregion