@verbaly/compiler 0.21.0 → 0.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -189,9 +189,12 @@ declare function doctor(cfg: ResolvedConfig): Promise<DoctorResult>;
189
189
  //#endregion
190
190
  //#region src/exchange.d.ts
191
191
  type ExchangeFormat = 'xliff' | 'csv';
192
+ type MobileFormat = 'android-xml' | 'ios-strings';
193
+ type ExportFormat = ExchangeFormat | MobileFormat;
194
+ declare function isMobileFormat(format: ExportFormat): format is MobileFormat;
192
195
  interface ExportOptions {
193
196
  locales?: string[];
194
- format?: ExchangeFormat;
197
+ format?: ExportFormat;
195
198
  out?: string;
196
199
  missing?: boolean;
197
200
  }
@@ -202,7 +205,7 @@ interface ExportedFile {
202
205
  untranslated: number;
203
206
  }
204
207
  interface ExportResult {
205
- format: ExchangeFormat;
208
+ format: ExportFormat;
206
209
  dir: string;
207
210
  files: ExportedFile[];
208
211
  }
@@ -309,5 +312,25 @@ interface RenderSiteResult {
309
312
  }
310
313
  declare function renderSite(cfg: ResolvedConfig, options?: RenderSiteOptions): Promise<RenderSiteResult>;
311
314
  //#endregion
312
- export { type Alternate, type Analysis, type AnalyzeOptions, type Catalog, type Catalogs, type CheckResult, type ClaudeProviderOptions, type DoctorEntry, type DoctorResult, type ExchangeFormat, type ExportOptions, type ExportResult, type ExportedFile, type ImportOptions, type ImportResult, type InitOptions, type InitResult, LOCALE_MODULE_PREFIX, MessageRegistry, type MissingEntry, PSEUDO_LOCALE, type ParamType, type PluginOptions, RESOLVED_VIRTUAL_ID, type RenderConfig, type RenderHtmlOptions, type RenderHtmlResult, type RenderSiteOptions, type RenderSiteResult, type ResolvedConfig, type RuntimeModuleOptions, SFC_FILE_RE, SOURCE_FILE_RE, type SyncResult, type TaggedMessage, type TaggedParam, type TransComponent, type TransformResult, type TranslateConfig, type TranslateOptions, type TranslateProvider, type TranslateRequest, type TranslateResult, type UnknownEntry, type UsedKey, VIRTUAL_ID, type VerbalyConfig, analyze, analyzeFile, analyzeSfc, catalogPath, check, claudeProvider, collectParams, detectBundler, doctor, exportCatalogs, extractProject, findConfigFile, formatCheckResult, generateDts, generateLocaleModule, generateRuntimeModule, importCatalogs, init, isTransformTarget, loadCatalogs, loadConfig, loadConfigFile, loadVirtualModule, parseExchangeFile, pruneCatalogs, pseudoCatalogs, pseudoLocalize, readCatalog, renderHtml, renderParamType, renderSite, resolveConfig, resolveVirtualId, runBuildGate, serializeCatalog, stableKey, structureMatches, syncCatalogs, targetLocales, transformCode, transformSource, translateCatalogs, writeCatalog, writeDts };
315
+ //#region src/status.d.ts
316
+ interface LocaleStatus {
317
+ locale: string;
318
+ translated: number;
319
+ total: number;
320
+ }
321
+ interface StatusResult {
322
+ messages: number;
323
+ source: string;
324
+ locales: LocaleStatus[];
325
+ }
326
+ declare function status(cfg: ResolvedConfig, catalogs: Catalogs, registry: MessageRegistry): StatusResult;
327
+ declare function formatStatusResult(result: StatusResult): string;
328
+ //#endregion
329
+ //#region src/watch.d.ts
330
+ interface WatchProjectOptions {
331
+ debounce?: number;
332
+ }
333
+ declare function watchProject(cfg: ResolvedConfig, run: () => Promise<void>, options?: WatchProjectOptions): () => void;
334
+ //#endregion
335
+ export { type Alternate, type Analysis, type AnalyzeOptions, type Catalog, type Catalogs, type CheckResult, type ClaudeProviderOptions, type DoctorEntry, type DoctorResult, type ExchangeFormat, type ExportFormat, type ExportOptions, type ExportResult, type ExportedFile, type ImportOptions, type ImportResult, type InitOptions, type InitResult, LOCALE_MODULE_PREFIX, type LocaleStatus, MessageRegistry, type MissingEntry, type MobileFormat, PSEUDO_LOCALE, type ParamType, type PluginOptions, RESOLVED_VIRTUAL_ID, type RenderConfig, type RenderHtmlOptions, type RenderHtmlResult, type RenderSiteOptions, type RenderSiteResult, type ResolvedConfig, type RuntimeModuleOptions, SFC_FILE_RE, SOURCE_FILE_RE, type StatusResult, type SyncResult, type TaggedMessage, type TaggedParam, type TransComponent, type TransformResult, type TranslateConfig, type TranslateOptions, type TranslateProvider, type TranslateRequest, type TranslateResult, type UnknownEntry, type UsedKey, VIRTUAL_ID, type VerbalyConfig, type WatchProjectOptions, analyze, analyzeFile, analyzeSfc, catalogPath, check, claudeProvider, collectParams, detectBundler, doctor, exportCatalogs, extractProject, findConfigFile, formatCheckResult, formatStatusResult, generateDts, generateLocaleModule, generateRuntimeModule, importCatalogs, init, isMobileFormat, isTransformTarget, loadCatalogs, loadConfig, loadConfigFile, loadVirtualModule, parseExchangeFile, pruneCatalogs, pseudoCatalogs, pseudoLocalize, readCatalog, renderHtml, renderParamType, renderSite, resolveConfig, resolveVirtualId, runBuildGate, serializeCatalog, stableKey, status, structureMatches, syncCatalogs, targetLocales, transformCode, transformSource, translateCatalogs, watchProject, writeCatalog, writeDts };
313
336
  //# sourceMappingURL=index.d.ts.map
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { parse } from "@babel/parser";
2
2
  import { createHash } from "node:crypto";
3
- import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
3
+ import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, watch, writeFileSync } from "node:fs";
4
4
  import { basename, dirname, join, relative, resolve } from "node:path";
5
5
  import { RICH_TAGS, createVerbaly, parse as parse$1, parseTags, safeHref } from "verbaly";
6
6
  import { pathToFileURL } from "node:url";
@@ -382,11 +382,17 @@ function catalogPath(cfg, locale) {
382
382
  return join(cfg.dir, `${locale}.json`);
383
383
  }
384
384
  function readCatalog(cfg, locale) {
385
+ let content;
385
386
  try {
386
- return JSON.parse(readFileSync(catalogPath(cfg, locale), "utf8"));
387
+ content = readFileSync(catalogPath(cfg, locale), "utf8");
387
388
  } catch {
388
389
  return {};
389
390
  }
391
+ try {
392
+ return JSON.parse(content.replace(/^\uFEFF/, ""));
393
+ } catch (error) {
394
+ throw new Error(`[verbaly] ${catalogPath(cfg, locale)} is not valid JSON, fix or delete the file`, { cause: error });
395
+ }
390
396
  }
391
397
  function loadCatalogs(cfg) {
392
398
  const catalogs = {};
@@ -403,8 +409,13 @@ function serializeCatalog(catalog) {
403
409
  }
404
410
  function writeCatalog(cfg, locale, catalog) {
405
411
  const serialized = serializeCatalog(catalog);
406
- mkdirSync(cfg.dir, { recursive: true });
407
- writeFileSync(catalogPath(cfg, locale), serialized);
412
+ const path = catalogPath(cfg, locale);
413
+ try {
414
+ if (readFileSync(path, "utf8") === serialized) return serialized;
415
+ } catch {
416
+ mkdirSync(cfg.dir, { recursive: true });
417
+ }
418
+ writeFileSync(path, serialized);
408
419
  return serialized;
409
420
  }
410
421
  //#endregion
@@ -1124,6 +1135,45 @@ function readDeps(root) {
1124
1135
  }
1125
1136
  }
1126
1137
  //#endregion
1138
+ //#region src/mobile.ts
1139
+ function androidValuesDir(locale, sourceLocale) {
1140
+ if (locale === sourceLocale) return "values";
1141
+ const parts = locale.split("-");
1142
+ if (parts.length === 1) return `values-${locale}`;
1143
+ if (parts.length === 2 && /^[a-zA-Z]{2}$/.test(parts[1])) return `values-${parts[0]}-r${parts[1].toUpperCase()}`;
1144
+ return `values-b+${parts.join("+")}`;
1145
+ }
1146
+ function toAndroidXml(entries) {
1147
+ const names = /* @__PURE__ */ new Map();
1148
+ return [
1149
+ "<?xml version=\"1.0\" encoding=\"UTF-8\"?>",
1150
+ "<resources>",
1151
+ ...entries.map(({ key, text }) => {
1152
+ const name = androidName(key);
1153
+ const clash = names.get(name);
1154
+ if (clash !== void 0) throw new Error(`[verbaly] android-xml: keys "${clash}" and "${key}" both become resource name "${name}", rename one of them.`);
1155
+ names.set(name, key);
1156
+ return ` <string name="${name}">${androidText(text)}</string>`;
1157
+ }),
1158
+ "</resources>",
1159
+ ""
1160
+ ].join("\n");
1161
+ }
1162
+ function toIosStrings(entries) {
1163
+ return [...entries.map(({ key, text }) => `"${iosText(key)}" = "${iosText(text)}";`), ""].join("\n");
1164
+ }
1165
+ function androidName(key) {
1166
+ const name = key.replace(/[^A-Za-z0-9_]/g, "_");
1167
+ return /^[0-9]/.test(name) ? `_${name}` : name;
1168
+ }
1169
+ function androidText(text) {
1170
+ const escaped = text.replace(/\\/g, "\\\\").replace(/'/g, "\\'").replace(/"/g, "\\\"").replace(/\n/g, "\\n").replace(/\t/g, "\\t").replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
1171
+ return /^[@?]/.test(escaped) ? `\\${escaped}` : escaped;
1172
+ }
1173
+ function iosText(text) {
1174
+ return text.replace(/\\/g, "\\\\").replace(/"/g, "\\\"").replace(/\n/g, "\\n").replace(/\t/g, "\\t");
1175
+ }
1176
+ //#endregion
1127
1177
  //#region src/translate.ts
1128
1178
  async function translateCatalogs(cfg, catalogs, provider, options = {}) {
1129
1179
  const batchSize = options.batchSize ?? 20;
@@ -1182,11 +1232,15 @@ function sameMembers(a, b) {
1182
1232
  }
1183
1233
  //#endregion
1184
1234
  //#region src/exchange.ts
1235
+ function isMobileFormat(format) {
1236
+ return format === "android-xml" || format === "ios-strings";
1237
+ }
1185
1238
  function exportCatalogs(cfg, catalogs, options = {}) {
1186
1239
  const format = options.format ?? "xliff";
1187
1240
  const dir = resolve(cfg.root, options.out ?? "verbaly-export");
1188
1241
  const source = catalogs[cfg.sourceLocale] ?? {};
1189
1242
  const targets = targetLocales(cfg, options.locales);
1243
+ if (isMobileFormat(format)) return exportMobile(cfg, catalogs, format, dir, source, targets);
1190
1244
  const files = [];
1191
1245
  mkdirSync(dir, { recursive: true });
1192
1246
  for (const locale of targets) {
@@ -1213,6 +1267,31 @@ function exportCatalogs(cfg, catalogs, options = {}) {
1213
1267
  files
1214
1268
  };
1215
1269
  }
1270
+ function exportMobile(cfg, catalogs, format, dir, source, targets) {
1271
+ const keys = Object.keys(source).filter((key) => source[key]).sort();
1272
+ const files = [];
1273
+ for (const locale of [cfg.sourceLocale, ...targets]) {
1274
+ const catalog = locale === cfg.sourceLocale ? source : catalogs[locale] ?? {};
1275
+ const entries = keys.map((key) => ({
1276
+ key,
1277
+ text: catalog[key] ?? ""
1278
+ })).filter((entry) => entry.text);
1279
+ const path = join(dir, format === "android-xml" ? join(androidValuesDir(locale, cfg.sourceLocale), "strings.xml") : join(`${locale}.lproj`, "Localizable.strings"));
1280
+ mkdirSync(dirname(path), { recursive: true });
1281
+ writeFileSync(path, format === "android-xml" ? toAndroidXml(entries) : toIosStrings(entries));
1282
+ files.push({
1283
+ locale,
1284
+ path,
1285
+ total: entries.length,
1286
+ untranslated: keys.length - entries.length
1287
+ });
1288
+ }
1289
+ return {
1290
+ format,
1291
+ dir,
1292
+ files
1293
+ };
1294
+ }
1216
1295
  function importCatalogs(cfg, catalogs, files, options = {}) {
1217
1296
  const source = catalogs[cfg.sourceLocale] ?? {};
1218
1297
  const result = {
@@ -1683,6 +1762,82 @@ function decodeEntities(text) {
1683
1762
  return text.replace(/&quot;/g, "\"").replace(/&#39;/g, "'").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&amp;/g, "&");
1684
1763
  }
1685
1764
  //#endregion
1686
- export { LOCALE_MODULE_PREFIX, MessageRegistry, PSEUDO_LOCALE, RESOLVED_VIRTUAL_ID, SFC_FILE_RE, SOURCE_FILE_RE, VIRTUAL_ID, analyze, analyzeFile, analyzeSfc, catalogPath, check, claudeProvider, collectParams, detectBundler, doctor, exportCatalogs, extractProject, findConfigFile, formatCheckResult, generateDts, generateLocaleModule, generateRuntimeModule, importCatalogs, init, isTransformTarget, loadCatalogs, loadConfig, loadConfigFile, loadVirtualModule, parseExchangeFile, pruneCatalogs, pseudoCatalogs, pseudoLocalize, readCatalog, renderHtml, renderParamType, renderSite, resolveConfig, resolveVirtualId, runBuildGate, serializeCatalog, stableKey, structureMatches, syncCatalogs, targetLocales, transformCode, transformSource, translateCatalogs, writeCatalog, writeDts };
1765
+ //#region src/status.ts
1766
+ function status(cfg, catalogs, registry) {
1767
+ const source = catalogs[cfg.sourceLocale] ?? {};
1768
+ const needed = /* @__PURE__ */ new Set([...registry.messages().keys(), ...Object.keys(source)]);
1769
+ const locales = [];
1770
+ for (const locale of cfg.locales) {
1771
+ if (locale === cfg.sourceLocale) continue;
1772
+ let translated = 0;
1773
+ for (const key of needed) if (catalogs[locale]?.[key]) translated += 1;
1774
+ locales.push({
1775
+ locale,
1776
+ translated,
1777
+ total: needed.size
1778
+ });
1779
+ }
1780
+ return {
1781
+ messages: needed.size,
1782
+ source: cfg.sourceLocale,
1783
+ locales
1784
+ };
1785
+ }
1786
+ function formatStatusResult(result) {
1787
+ const lines = [`[verbaly] ${result.messages} messages · source: ${result.source}`];
1788
+ if (result.locales.length === 0) {
1789
+ lines.push(" no target locales (add locales to your config)");
1790
+ return lines.join("\n");
1791
+ }
1792
+ for (const { locale, translated, total } of result.locales) {
1793
+ const pct = total === 0 ? 100 : Math.floor(translated / total * 100);
1794
+ const mark = translated === total ? " ✓" : "";
1795
+ lines.push(` ${locale}: ${translated}/${total} translated (${pct}%)${mark}`);
1796
+ }
1797
+ return lines.join("\n");
1798
+ }
1799
+ //#endregion
1800
+ //#region src/watch.ts
1801
+ function watchProject(cfg, run, options = {}) {
1802
+ const catalogDir = relative(cfg.root, cfg.dir).replaceAll("\\", "/");
1803
+ let timer;
1804
+ let running = false;
1805
+ let queued = false;
1806
+ async function refresh() {
1807
+ if (running) {
1808
+ queued = true;
1809
+ return;
1810
+ }
1811
+ running = true;
1812
+ try {
1813
+ await run();
1814
+ } catch (error) {
1815
+ console.warn("[verbaly] watch run failed:", error);
1816
+ } finally {
1817
+ running = false;
1818
+ if (queued) {
1819
+ queued = false;
1820
+ schedule();
1821
+ }
1822
+ }
1823
+ }
1824
+ function schedule() {
1825
+ clearTimeout(timer);
1826
+ timer = setTimeout(() => void refresh(), options.debounce ?? 150);
1827
+ }
1828
+ const watcher = watch(cfg.root, { recursive: true }, (_event, filename) => {
1829
+ if (!filename) return;
1830
+ const file = filename.replaceAll("\\", "/");
1831
+ if (file.includes("node_modules/") || file.endsWith(".d.ts")) return;
1832
+ if (catalogDir && file.startsWith(`${catalogDir}/`)) return;
1833
+ if (SOURCE_FILE_RE.test(file)) schedule();
1834
+ });
1835
+ return () => {
1836
+ clearTimeout(timer);
1837
+ watcher.close();
1838
+ };
1839
+ }
1840
+ //#endregion
1841
+ export { LOCALE_MODULE_PREFIX, MessageRegistry, PSEUDO_LOCALE, RESOLVED_VIRTUAL_ID, SFC_FILE_RE, SOURCE_FILE_RE, VIRTUAL_ID, analyze, analyzeFile, analyzeSfc, catalogPath, check, claudeProvider, collectParams, detectBundler, doctor, exportCatalogs, extractProject, findConfigFile, formatCheckResult, formatStatusResult, generateDts, generateLocaleModule, generateRuntimeModule, importCatalogs, init, isMobileFormat, isTransformTarget, loadCatalogs, loadConfig, loadConfigFile, loadVirtualModule, parseExchangeFile, pruneCatalogs, pseudoCatalogs, pseudoLocalize, readCatalog, renderHtml, renderParamType, renderSite, resolveConfig, resolveVirtualId, runBuildGate, serializeCatalog, stableKey, status, structureMatches, syncCatalogs, targetLocales, transformCode, transformSource, translateCatalogs, watchProject, writeCatalog, writeDts };
1687
1842
 
1688
1843
  //# sourceMappingURL=index.js.map