@verbaly/compiler 0.24.0 → 0.25.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
@@ -50,11 +50,11 @@ Catalogs are **flat JSON**: most TMS platforms (Crowdin, Lokalise, Phrase, …)
50
50
 
51
51
  ```bash
52
52
  npx verbaly export # verbaly-export/<locale>.xlf (XLIFF 2.0, source + target per unit)
53
- npx verbaly export --format csv # spreadsheet-friendly: key,source,target
53
+ npx verbaly export --format csv # spreadsheet-friendly: key,source,target,location
54
54
  npx verbaly import verbaly-export/es.xlf # fill the catalog back
55
55
  ```
56
56
 
57
- `export` writes one file per target locale with the source text alongside the current translation (`--missing` exports only the untranslated entries). `import` reads XLIFF 2.0/1.2 or CSV back and **validates every entry like `translate` does**: a translation that drops a `{param}`, a variant block or an `<em>` tag is rejected and reported, so a translator's typo can't break your UI. Existing translations are kept unless `--overwrite`; `--dry-run` previews everything.
57
+ `export` writes one file per target locale with the source text alongside the current translation (`--missing` exports only the untranslated entries). Every entry carries **where the text lives in your source** (XLIFF `location` notes, a `location` column in CSV), so translators and TMS tools see the context instead of guessing it. `import` reads XLIFF 2.0/1.2 or CSV back and **validates every entry like `translate` does**: a translation that drops a `{param}`, a variant block or an `<em>` tag is rejected and reported, so a translator's typo can't break your UI. Existing translations are kept unless `--overwrite`; `--dry-run` previews everything.
58
58
 
59
59
  ## 📱 Mobile resources
60
60
 
package/dist/cli.js CHANGED
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
+ import { basename, dirname, join, relative, resolve } from "node:path";
2
3
  import { parseArgs } from "node:util";
3
4
  import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, watch, writeFileSync } from "node:fs";
4
- import { basename, dirname, join, relative, resolve } from "node:path";
5
- import { RICH_TAGS, createVerbaly, normalizeLink, parse, parseTags, safeAttribute } from "verbaly";
5
+ import { RICH_TAGS, createVerbaly, localeDirection, normalizeLink, parse, parseTags, safeAttribute } from "verbaly";
6
6
  import { pathToFileURL } from "node:url";
7
7
  import { glob } from "tinyglobby";
8
8
  import { parse as parse$1 } from "@babel/parser";
@@ -427,6 +427,15 @@ var MessageRegistry = class {
427
427
  }
428
428
  return out;
429
429
  }
430
+ origins() {
431
+ const out = this.usedKeys();
432
+ for (const analysis of this.files.values()) for (const msg of analysis.tagged) {
433
+ const files = out.get(msg.key) ?? [];
434
+ if (!files.includes(msg.file)) files.push(msg.file);
435
+ out.set(msg.key, files);
436
+ }
437
+ return out;
438
+ }
430
439
  };
431
440
  //#endregion
432
441
  //#region src/key.ts
@@ -1128,7 +1137,8 @@ function exportCatalogs(cfg, catalogs, options = {}) {
1128
1137
  const all = Object.keys(source).filter((key) => source[key]).sort().map((key) => ({
1129
1138
  key,
1130
1139
  source: source[key],
1131
- target: catalog[key] ?? ""
1140
+ target: catalog[key] ?? "",
1141
+ location: options.origins?.[key]
1132
1142
  }));
1133
1143
  const untranslated = all.filter((entry) => !entry.target);
1134
1144
  const entries = options.missing ? untranslated : all;
@@ -1224,8 +1234,13 @@ function parseExchangeFile(file, localeOverride) {
1224
1234
  throw new Error(`[verbaly] ${file}: unsupported format, expected .xlf, .xliff or .csv.`);
1225
1235
  }
1226
1236
  function toXliff(sourceLocale, locale, entries) {
1227
- const units = entries.map(({ key, source, target }) => [
1237
+ const units = entries.map(({ key, source, target, location }) => [
1228
1238
  ` <unit id="${escapeXml(key)}">`,
1239
+ ...location?.length ? [
1240
+ " <notes>",
1241
+ ...location.map((file) => ` <note category="location">${escapeXml(file)}</note>`),
1242
+ " </notes>"
1243
+ ] : [],
1229
1244
  ` <segment state="${target ? "translated" : "initial"}">`,
1230
1245
  ` <source>${escapeXml(source)}</source>`,
1231
1246
  ` <target>${escapeXml(target)}</target>`,
@@ -1275,8 +1290,8 @@ function unescapeXml(text) {
1275
1290
  }
1276
1291
  function toCsv(entries) {
1277
1292
  return [
1278
- "key,source,target",
1279
- ...entries.map(({ key, source, target }) => `${csvField(key)},${csvField(source)},${csvField(target)}`),
1293
+ "key,source,target,location",
1294
+ ...entries.map(({ key, source, target, location }) => `${csvField(key)},${csvField(source)},${csvField(target)},${csvField(location?.join("; ") ?? "")}`),
1280
1295
  ""
1281
1296
  ].join("\r\n");
1282
1297
  }
@@ -1378,6 +1393,7 @@ function renderHtml(html, options) {
1378
1393
  const chunkStart = m.index + 1 + rawName.length;
1379
1394
  if (tagName === "html" && options.setLang !== false) {
1380
1395
  setAttribute(ms, html, chunkStart, openEnd, attrChunk, "lang", options.locale);
1396
+ setAttribute(ms, html, chunkStart, openEnd, attrChunk, "dir", localeDirection(options.locale));
1381
1397
  continue;
1382
1398
  }
1383
1399
  const attrs = parseAttrs(attrChunk);
@@ -1876,7 +1892,8 @@ async function runCli(args = process.argv.slice(2)) {
1876
1892
  locales: values.locales?.split(","),
1877
1893
  format,
1878
1894
  out: values.out,
1879
- missing: values.missing
1895
+ missing: values.missing,
1896
+ origins: isMobileFormat(format) ? void 0 : await collectOrigins(cfg)
1880
1897
  });
1881
1898
  if (result.files.length === 0) {
1882
1899
  console.log("[verbaly] no target locales to export (add locales to your config)");
@@ -1982,6 +1999,12 @@ function rejectStrayFlags(command, values) {
1982
1999
  process.exitCode = 1;
1983
2000
  return true;
1984
2001
  }
2002
+ async function collectOrigins(cfg) {
2003
+ const registry = await extractProject(cfg);
2004
+ const origins = {};
2005
+ for (const [key, files] of registry.origins()) origins[key] = files.map((file) => relative(cfg.root, file).replaceAll("\\", "/"));
2006
+ return origins;
2007
+ }
1985
2008
  async function resolveProvider(cfg, model) {
1986
2009
  const configured = cfg.translate.provider;
1987
2010
  if (typeof configured === "function") return configured;