@verbaly/compiler 0.20.0 → 0.22.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
@@ -148,17 +148,31 @@ interface RuntimeModuleOptions {
148
148
  }
149
149
  declare function generateRuntimeModule(cfg: ResolvedConfig, options?: RuntimeModuleOptions): string;
150
150
  declare function generateLocaleModule(catalog: Catalog): string;
151
- declare function generateDts(entries: Map<string, string>): string;
152
- declare function writeDts(cfg: ResolvedConfig, entries: Map<string, string>): void;
151
+ declare function generateDts(catalog: Catalog): string;
152
+ declare function writeDts(cfg: ResolvedConfig, catalog: Catalog): void;
153
+ //#endregion
154
+ //#region src/transform.d.ts
155
+ interface TransformResult {
156
+ code: string;
157
+ map: ReturnType<MagicString['generateMap']>;
158
+ }
159
+ declare function transformCode(code: string, file: string, analysis?: Analysis): TransformResult | null;
153
160
  //#endregion
154
161
  //#region src/plugin.d.ts
162
+ interface PluginOptions extends VerbalyConfig {
163
+ failOnMissing?: boolean;
164
+ }
155
165
  declare const RESOLVED_VIRTUAL_ID: string;
156
166
  declare const LOCALE_MODULE_PREFIX = "\0virtual:verbaly/locale/";
157
167
  declare const SOURCE_FILE_RE: RegExp;
158
168
  declare function resolveVirtualId(id: string): string | undefined;
159
169
  declare function loadVirtualModule(id: string, cfg: ResolvedConfig, catalogs: Catalogs): string | undefined;
160
170
  declare function isTransformTarget(id: string): boolean;
161
- declare function runBuildGate(cfg: ResolvedConfig, registry: MessageRegistry): void;
171
+ declare function transformSource(code: string, id: string, registry: MessageRegistry): {
172
+ analysis: Analysis;
173
+ result: TransformResult | null;
174
+ };
175
+ declare function runBuildGate(cfg: ResolvedConfig, registry: MessageRegistry, failOnMissing?: boolean): void;
162
176
  //#endregion
163
177
  //#region src/doctor.d.ts
164
178
  interface DoctorEntry {
@@ -175,9 +189,12 @@ declare function doctor(cfg: ResolvedConfig): Promise<DoctorResult>;
175
189
  //#endregion
176
190
  //#region src/exchange.d.ts
177
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;
178
195
  interface ExportOptions {
179
196
  locales?: string[];
180
- format?: ExchangeFormat;
197
+ format?: ExportFormat;
181
198
  out?: string;
182
199
  missing?: boolean;
183
200
  }
@@ -188,7 +205,7 @@ interface ExportedFile {
188
205
  untranslated: number;
189
206
  }
190
207
  interface ExportResult {
191
- format: ExchangeFormat;
208
+ format: ExportFormat;
192
209
  dir: string;
193
210
  files: ExportedFile[];
194
211
  }
@@ -295,12 +312,5 @@ interface RenderSiteResult {
295
312
  }
296
313
  declare function renderSite(cfg: ResolvedConfig, options?: RenderSiteOptions): Promise<RenderSiteResult>;
297
314
  //#endregion
298
- //#region src/transform.d.ts
299
- interface TransformResult {
300
- code: string;
301
- map: ReturnType<MagicString['generateMap']>;
302
- }
303
- declare function transformCode(code: string, file: string, analysis?: Analysis): TransformResult | null;
304
- //#endregion
305
- 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, 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, translateCatalogs, writeCatalog, writeDts };
315
+ 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, 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 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, isMobileFormat, 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 };
306
316
  //# sourceMappingURL=index.d.ts.map
package/dist/index.js CHANGED
@@ -4,8 +4,8 @@ import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync
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";
7
- import { glob } from "tinyglobby";
8
7
  import MagicString from "magic-string";
8
+ import { glob } from "tinyglobby";
9
9
  //#region src/key.ts
10
10
  function stableKey(message) {
11
11
  return createHash("sha256").update(message).digest("base64url").slice(0, 8);
@@ -69,6 +69,20 @@ function handleTrans(code, node, file, tagged, usedKeys, names) {
69
69
  const attrs = opening.attributes;
70
70
  const idAttr = attrs.find((a) => a.type === "JSXAttribute" && a.name.name === "id");
71
71
  const children = node.children;
72
+ const push = (key, built) => tagged.push({
73
+ key,
74
+ message: built.text,
75
+ params: built.params,
76
+ start: node.start,
77
+ end: node.end,
78
+ tagStart: nameNode.start,
79
+ tagEnd: nameNode.end,
80
+ file,
81
+ jsx: {
82
+ name: nameNode.name,
83
+ components: built.components
84
+ }
85
+ });
72
86
  if (idAttr) {
73
87
  const value = idAttr.value;
74
88
  if (value?.type !== "StringLiteral") return;
@@ -76,20 +90,7 @@ function handleTrans(code, node, file, tagged, usedKeys, names) {
76
90
  if (attrs.length === 1 && children?.length) {
77
91
  const built = buildTransMessage(code, children, names);
78
92
  if (built?.text.trim()) {
79
- tagged.push({
80
- key: id,
81
- message: built.text,
82
- params: built.params,
83
- start: node.start,
84
- end: node.end,
85
- tagStart: nameNode.start,
86
- tagEnd: nameNode.end,
87
- file,
88
- jsx: {
89
- name: nameNode.name,
90
- components: built.components
91
- }
92
- });
93
+ push(id, built);
93
94
  return;
94
95
  }
95
96
  }
@@ -103,20 +104,7 @@ function handleTrans(code, node, file, tagged, usedKeys, names) {
103
104
  if (!children?.length) return;
104
105
  const built = buildTransMessage(code, children, names);
105
106
  if (!built || !built.text.trim()) return;
106
- tagged.push({
107
- key: stableKey(built.text),
108
- message: built.text,
109
- params: built.params,
110
- start: node.start,
111
- end: node.end,
112
- tagStart: nameNode.start,
113
- tagEnd: nameNode.end,
114
- file,
115
- jsx: {
116
- name: nameNode.name,
117
- components: built.components
118
- }
119
- });
107
+ push(stableKey(built.text), built);
120
108
  }
121
109
  function explicitId(tag, names) {
122
110
  if (tag.type !== "CallExpression") return void 0;
@@ -272,8 +260,9 @@ function uniqueName(base, source, taken) {
272
260
  }
273
261
  function walk(node, visit) {
274
262
  visit(node);
275
- for (const [key, value] of Object.entries(node)) {
263
+ for (const key in node) {
276
264
  if (SKIP_KEYS.has(key)) continue;
265
+ const value = node[key];
277
266
  if (Array.isArray(value)) {
278
267
  for (const item of value) if (isNode(item)) walk(item, visit);
279
268
  } else if (isNode(value)) walk(value, visit);
@@ -393,11 +382,17 @@ function catalogPath(cfg, locale) {
393
382
  return join(cfg.dir, `${locale}.json`);
394
383
  }
395
384
  function readCatalog(cfg, locale) {
385
+ let content;
396
386
  try {
397
- return JSON.parse(readFileSync(catalogPath(cfg, locale), "utf8"));
387
+ content = readFileSync(catalogPath(cfg, locale), "utf8");
398
388
  } catch {
399
389
  return {};
400
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
+ }
401
396
  }
402
397
  function loadCatalogs(cfg) {
403
398
  const catalogs = {};
@@ -454,13 +449,13 @@ function formatCheckResult(result) {
454
449
  if (result.missing.length > 0) {
455
450
  lines.push("missing translations:");
456
451
  for (const entry of result.missing) {
457
- const hint = entry.source ? ` "${truncate(entry.source, 40)}"` : "";
452
+ const hint = entry.source ? `: "${truncate(entry.source, 40)}"` : "";
458
453
  lines.push(` [${entry.locale}] ${entry.key}${hint}`);
459
454
  }
460
455
  }
461
456
  if (result.unknown.length > 0) {
462
457
  lines.push("unknown keys (not in any catalog):");
463
- for (const entry of result.unknown) lines.push(` ${entry.key} used in ${entry.files.join(", ")}`);
458
+ for (const entry of result.unknown) lines.push(` ${entry.key} (used in ${entry.files.join(", ")})`);
464
459
  }
465
460
  return lines.join("\n");
466
461
  }
@@ -507,11 +502,15 @@ function visit(nodes, out) {
507
502
  }
508
503
  function renderParamType(types) {
509
504
  if (types.has("unknown") || types.size === 0) return "unknown";
510
- const parts = [];
511
- if (types.has("number")) parts.push("number");
512
- if (types.has("string")) parts.push("string");
513
- if (types.has("date")) parts.push("Date | number | string");
514
- return parts.join(" | ");
505
+ const members = /* @__PURE__ */ new Set();
506
+ if (types.has("number")) members.add("number");
507
+ if (types.has("string")) members.add("string");
508
+ if (types.has("date")) for (const m of [
509
+ "Date",
510
+ "number",
511
+ "string"
512
+ ]) members.add(m);
513
+ return [...members].join(" | ");
515
514
  }
516
515
  //#endregion
517
516
  //#region src/codegen.ts
@@ -578,9 +577,9 @@ ${options.extraExports ?? ""}`;
578
577
  function generateLocaleModule(catalog) {
579
578
  return `export default ${JSON.stringify(catalog)};\n`;
580
579
  }
581
- function generateDts(entries) {
580
+ function generateDts(catalog) {
582
581
  const lines = [];
583
- for (const [key, message] of [...entries.entries()].sort(([a], [b]) => a.localeCompare(b))) {
582
+ for (const [key, message] of Object.entries(catalog).sort(([a], [b]) => a.localeCompare(b))) {
584
583
  const params = collectParams(message);
585
584
  if (params.size === 0) lines.push(` ${JSON.stringify(key)}: never;`);
586
585
  else {
@@ -629,8 +628,13 @@ declare module 'virtual:verbaly/locale/*' {
629
628
  }
630
629
  `;
631
630
  }
632
- function writeDts(cfg, entries) {
633
- writeFileSync(join(cfg.root, "verbaly.d.ts"), generateDts(entries));
631
+ function writeDts(cfg, catalog) {
632
+ const file = join(cfg.root, "verbaly.d.ts");
633
+ const content = generateDts(catalog);
634
+ try {
635
+ if (readFileSync(file, "utf8") === content) return;
636
+ } catch {}
637
+ writeFileSync(file, content);
634
638
  }
635
639
  //#endregion
636
640
  //#region src/pseudo.ts
@@ -801,7 +805,7 @@ async function loadTsConfig(path) {
801
805
  const { mod } = await bundleRequire({ filepath: path });
802
806
  return mod.default ?? {};
803
807
  } catch (error) {
804
- if (isModuleNotFound(error, "esbuild")) throw new Error(`[verbaly] ${path} needs esbuild install it: pnpm add -D esbuild`, { cause: error });
808
+ if (isModuleNotFound(error, "esbuild")) throw new Error(`[verbaly] ${path} needs esbuild: install it with pnpm add -D esbuild`, { cause: error });
805
809
  throw error;
806
810
  }
807
811
  }
@@ -822,6 +826,35 @@ function definedOnly(config) {
822
826
  return Object.fromEntries(Object.entries(config).filter(([, value]) => value !== void 0));
823
827
  }
824
828
  //#endregion
829
+ //#region src/transform.ts
830
+ function quote(text, single) {
831
+ if (!single) return JSON.stringify(text);
832
+ return `'${text.replace(/\\/g, "\\\\").replace(/'/g, "\\'")}'`;
833
+ }
834
+ function transformCode(code, file, analysis) {
835
+ const { tagged } = analysis ?? analyzeFile(code, file);
836
+ if (tagged.length === 0) return null;
837
+ const s = new MagicString(code);
838
+ for (const msg of tagged) {
839
+ const seen = /* @__PURE__ */ new Set();
840
+ const entries = msg.params.filter((p) => !seen.has(p.name) && seen.add(p.name));
841
+ const pairs = entries.map((p) => `${quote(p.name, msg.singleQuote)}: ${code.slice(p.start, p.end)}`).join(", ");
842
+ if (msg.jsx) {
843
+ const values = entries.length ? ` values={{ ${pairs} }}` : "";
844
+ const components = msg.jsx.components.length ? ` components={{ ${msg.jsx.components.map((c) => `${JSON.stringify(c.name)}: ${c.source}`).join(", ")} }}` : "";
845
+ s.overwrite(msg.start, msg.end, `<${msg.jsx.name} id=${JSON.stringify(msg.key)}${values}${components} />`);
846
+ continue;
847
+ }
848
+ const tagSource = code.slice(msg.tagStart, msg.tagEnd);
849
+ const args = entries.length ? `, { ${pairs} }` : "";
850
+ s.overwrite(msg.start, msg.end, `${tagSource}(${quote(msg.key, msg.singleQuote)}${args})`);
851
+ }
852
+ return {
853
+ code: s.toString(),
854
+ map: s.generateMap({ hires: true })
855
+ };
856
+ }
857
+ //#endregion
825
858
  //#region src/plugin.ts
826
859
  const RESOLVED_VIRTUAL_ID = "\0virtual:verbaly";
827
860
  const LOCALE_MODULE_PREFIX = `${RESOLVED_VIRTUAL_ID}/locale/`;
@@ -836,7 +869,16 @@ function loadVirtualModule(id, cfg, catalogs) {
836
869
  function isTransformTarget(id) {
837
870
  return SOURCE_FILE_RE.test(id) && !id.includes("node_modules") && !id.startsWith("\0");
838
871
  }
839
- function runBuildGate(cfg, registry) {
872
+ function transformSource(code, id, registry) {
873
+ const analysis = analyzeFile(code, id);
874
+ registry.update(id, analysis);
875
+ return {
876
+ analysis,
877
+ result: transformCode(code, id, analysis) ?? null
878
+ };
879
+ }
880
+ function runBuildGate(cfg, registry, failOnMissing) {
881
+ if (failOnMissing === false) return;
840
882
  const result = check(cfg, loadCatalogs(cfg), registry);
841
883
  if (!result.ok) throw new Error(`[verbaly] build blocked\n${formatCheckResult(result)}\nRun \`npx verbaly extract\` and fill the missing translations.`);
842
884
  }
@@ -855,7 +897,7 @@ var MessageRegistry = class {
855
897
  for (const analysis of this.files.values()) for (const msg of analysis.tagged) {
856
898
  const existing = out.get(msg.key);
857
899
  if (existing) {
858
- if (existing.message !== msg.message) console.warn(`[verbaly] key collision "${msg.key}": ${JSON.stringify(existing.message)} vs ${JSON.stringify(msg.message)} second dropped.`);
900
+ if (existing.message !== msg.message) console.warn(`[verbaly] key collision "${msg.key}": ${JSON.stringify(existing.message)} vs ${JSON.stringify(msg.message)}, second dropped.`);
859
901
  continue;
860
902
  }
861
903
  out.set(msg.key, msg);
@@ -987,27 +1029,27 @@ function init(options = {}) {
987
1029
  const PREVIEW = 5;
988
1030
  async function doctor(cfg) {
989
1031
  const entries = [];
990
- const ok = (check, message) => entries.push({
1032
+ const ok = (name, message) => entries.push({
991
1033
  level: "ok",
992
- check,
1034
+ check: name,
993
1035
  message
994
1036
  });
995
- const warn = (check, message, fix) => entries.push({
1037
+ const warn = (name, message, fix) => entries.push({
996
1038
  level: "warn",
997
- check,
1039
+ check: name,
998
1040
  message,
999
1041
  fix
1000
1042
  });
1001
- const error = (check, message, fix) => entries.push({
1043
+ const error = (name, message, fix) => entries.push({
1002
1044
  level: "error",
1003
- check,
1045
+ check: name,
1004
1046
  message,
1005
1047
  fix
1006
1048
  });
1007
1049
  const rel = (path) => relative(cfg.root, path).replaceAll("\\", "/");
1008
1050
  const configFile = findConfigFile(cfg.root);
1009
1051
  if (configFile) ok("config", `${configFile} found`);
1010
- else warn("config", "no config file running on defaults", "run `npx verbaly init`");
1052
+ else warn("config", "no config file, running on defaults", "run `npx verbaly init`");
1011
1053
  const catalogs = {};
1012
1054
  let catalogsHealthy = true;
1013
1055
  if (!existsSync(cfg.dir)) {
@@ -1040,14 +1082,14 @@ async function doctor(cfg) {
1040
1082
  const deps = readDeps(cfg.root);
1041
1083
  const bundler = detectBundler(cfg.root);
1042
1084
  const wired = deps["@verbaly/vite"] || deps["@verbaly/unplugin"];
1043
- if (!bundler) ok("plugin", "no bundler detected CLI flow (extract/check) applies");
1085
+ if (!bundler) ok("plugin", "no bundler detected, the CLI flow (extract/check) applies");
1044
1086
  else if (wired) ok("plugin", `${deps["@verbaly/vite"] ? "@verbaly/vite" : "@verbaly/unplugin"} installed for ${bundler}`);
1045
1087
  else if (bundler === "vite") warn("plugin", "vite detected but @verbaly/vite is not installed", "pnpm add -D @verbaly/vite and add verbaly() to the plugins in vite.config");
1046
1088
  else warn("plugin", `${bundler} detected but @verbaly/unplugin is not installed`, `pnpm add -D @verbaly/unplugin and add the verbaly ${bundler} plugin to your build config`);
1047
1089
  if (source) {
1048
1090
  const dtsPath = join(cfg.root, "verbaly.d.ts");
1049
1091
  if (!existsSync(dtsPath)) warn("types", "verbaly.d.ts has not been generated", "run `npx verbaly extract`");
1050
- else if (readFileSync(dtsPath, "utf8") !== generateDts(new Map(Object.entries(source)))) warn("types", "verbaly.d.ts is stale", "run `npx verbaly extract`");
1092
+ else if (readFileSync(dtsPath, "utf8") !== generateDts(source)) warn("types", "verbaly.d.ts is stale", "run `npx verbaly extract`");
1051
1093
  else ok("types", "verbaly.d.ts is up to date");
1052
1094
  }
1053
1095
  const registry = await extractProject(cfg);
@@ -1060,10 +1102,10 @@ async function doctor(cfg) {
1060
1102
  }
1061
1103
  if (catalogsHealthy) {
1062
1104
  const result = check(cfg, catalogs, registry);
1063
- if (result.unknown.length > 0) error("keys", `${result.unknown.length} unknown ${result.unknown.length === 1 ? "key" : "keys"} used in code (${preview(result.unknown.map((u) => u.key))})`, "fix the key or add it to the catalogs `npx verbaly check` for details");
1105
+ if (result.unknown.length > 0) error("keys", `${result.unknown.length} unknown ${result.unknown.length === 1 ? "key" : "keys"} used in code (${preview(result.unknown.map((u) => u.key))})`, "fix the key or add it to the catalogs (`npx verbaly check` for details)");
1064
1106
  if (result.missing.length > 0) {
1065
1107
  const locales = [...new Set(result.missing.map((m) => m.locale))];
1066
- warn("translations", `${result.missing.length} missing ${result.missing.length === 1 ? "translation" : "translations"} (${locales.join(", ")})`, "run `npx verbaly translate` or fill the catalogs `npx verbaly check` for details");
1108
+ warn("translations", `${result.missing.length} missing ${result.missing.length === 1 ? "translation" : "translations"} (${locales.join(", ")})`, "run `npx verbaly translate` or fill the catalogs (`npx verbaly check` for details)");
1067
1109
  }
1068
1110
  if (result.ok) ok("translations", "all translations complete");
1069
1111
  }
@@ -1088,6 +1130,45 @@ function readDeps(root) {
1088
1130
  }
1089
1131
  }
1090
1132
  //#endregion
1133
+ //#region src/mobile.ts
1134
+ function androidValuesDir(locale, sourceLocale) {
1135
+ if (locale === sourceLocale) return "values";
1136
+ const parts = locale.split("-");
1137
+ if (parts.length === 1) return `values-${locale}`;
1138
+ if (parts.length === 2 && /^[a-zA-Z]{2}$/.test(parts[1])) return `values-${parts[0]}-r${parts[1].toUpperCase()}`;
1139
+ return `values-b+${parts.join("+")}`;
1140
+ }
1141
+ function toAndroidXml(entries) {
1142
+ const names = /* @__PURE__ */ new Map();
1143
+ return [
1144
+ "<?xml version=\"1.0\" encoding=\"UTF-8\"?>",
1145
+ "<resources>",
1146
+ ...entries.map(({ key, text }) => {
1147
+ const name = androidName(key);
1148
+ const clash = names.get(name);
1149
+ if (clash !== void 0) throw new Error(`[verbaly] android-xml: keys "${clash}" and "${key}" both become resource name "${name}", rename one of them.`);
1150
+ names.set(name, key);
1151
+ return ` <string name="${name}">${androidText(text)}</string>`;
1152
+ }),
1153
+ "</resources>",
1154
+ ""
1155
+ ].join("\n");
1156
+ }
1157
+ function toIosStrings(entries) {
1158
+ return [...entries.map(({ key, text }) => `"${iosText(key)}" = "${iosText(text)}";`), ""].join("\n");
1159
+ }
1160
+ function androidName(key) {
1161
+ const name = key.replace(/[^A-Za-z0-9_]/g, "_");
1162
+ return /^[0-9]/.test(name) ? `_${name}` : name;
1163
+ }
1164
+ function androidText(text) {
1165
+ 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;");
1166
+ return /^[@?]/.test(escaped) ? `\\${escaped}` : escaped;
1167
+ }
1168
+ function iosText(text) {
1169
+ return text.replace(/\\/g, "\\\\").replace(/"/g, "\\\"").replace(/\n/g, "\\n").replace(/\t/g, "\\t");
1170
+ }
1171
+ //#endregion
1091
1172
  //#region src/translate.ts
1092
1173
  async function translateCatalogs(cfg, catalogs, provider, options = {}) {
1093
1174
  const batchSize = options.batchSize ?? 20;
@@ -1146,28 +1227,58 @@ function sameMembers(a, b) {
1146
1227
  }
1147
1228
  //#endregion
1148
1229
  //#region src/exchange.ts
1230
+ function isMobileFormat(format) {
1231
+ return format === "android-xml" || format === "ios-strings";
1232
+ }
1149
1233
  function exportCatalogs(cfg, catalogs, options = {}) {
1150
1234
  const format = options.format ?? "xliff";
1151
1235
  const dir = resolve(cfg.root, options.out ?? "verbaly-export");
1152
1236
  const source = catalogs[cfg.sourceLocale] ?? {};
1153
1237
  const targets = targetLocales(cfg, options.locales);
1238
+ if (isMobileFormat(format)) return exportMobile(cfg, catalogs, format, dir, source, targets);
1154
1239
  const files = [];
1155
1240
  mkdirSync(dir, { recursive: true });
1156
1241
  for (const locale of targets) {
1157
1242
  const catalog = catalogs[locale] ?? {};
1158
- let entries = Object.keys(source).filter((key) => source[key]).sort().map((key) => ({
1243
+ const all = Object.keys(source).filter((key) => source[key]).sort().map((key) => ({
1159
1244
  key,
1160
1245
  source: source[key],
1161
1246
  target: catalog[key] ?? ""
1162
1247
  }));
1163
- if (options.missing) entries = entries.filter((entry) => !entry.target);
1248
+ const untranslated = all.filter((entry) => !entry.target);
1249
+ const entries = options.missing ? untranslated : all;
1164
1250
  const path = join(dir, `${locale}.${format === "csv" ? "csv" : "xlf"}`);
1165
1251
  writeFileSync(path, format === "csv" ? toCsv(entries) : toXliff(cfg.sourceLocale, locale, entries));
1166
1252
  files.push({
1167
1253
  locale,
1168
1254
  path,
1169
1255
  total: entries.length,
1170
- untranslated: entries.filter((entry) => !entry.target).length
1256
+ untranslated: untranslated.length
1257
+ });
1258
+ }
1259
+ return {
1260
+ format,
1261
+ dir,
1262
+ files
1263
+ };
1264
+ }
1265
+ function exportMobile(cfg, catalogs, format, dir, source, targets) {
1266
+ const keys = Object.keys(source).filter((key) => source[key]).sort();
1267
+ const files = [];
1268
+ for (const locale of [cfg.sourceLocale, ...targets]) {
1269
+ const catalog = locale === cfg.sourceLocale ? source : catalogs[locale] ?? {};
1270
+ const entries = keys.map((key) => ({
1271
+ key,
1272
+ text: catalog[key] ?? ""
1273
+ })).filter((entry) => entry.text);
1274
+ const path = join(dir, format === "android-xml" ? join(androidValuesDir(locale, cfg.sourceLocale), "strings.xml") : join(`${locale}.lproj`, "Localizable.strings"));
1275
+ mkdirSync(dirname(path), { recursive: true });
1276
+ writeFileSync(path, format === "android-xml" ? toAndroidXml(entries) : toIosStrings(entries));
1277
+ files.push({
1278
+ locale,
1279
+ path,
1280
+ total: entries.length,
1281
+ untranslated: keys.length - entries.length
1171
1282
  });
1172
1283
  }
1173
1284
  return {
@@ -1187,8 +1298,8 @@ function importCatalogs(cfg, catalogs, files, options = {}) {
1187
1298
  for (const file of files) {
1188
1299
  const parsed = parseExchangeFile(file, options.locale);
1189
1300
  const locale = parsed.locale;
1190
- if (!/^[a-zA-Z]{2,3}([-_][a-zA-Z0-9]+)*$/.test(locale)) throw new Error(`[verbaly] ${file}: "${locale}" doesn't look like a locale pass --locale <id>.`);
1191
- if (locale === cfg.sourceLocale) throw new Error(`[verbaly] ${file} targets the source locale "${locale}" import fills translations, not the source. Pass --locale if the detection is wrong.`);
1301
+ if (!/^[a-zA-Z]{2,3}([-_][a-zA-Z0-9]+)*$/.test(locale)) throw new Error(`[verbaly] ${file}: "${locale}" doesn't look like a locale, pass --locale <id>.`);
1302
+ if (locale === cfg.sourceLocale) throw new Error(`[verbaly] ${file} targets the source locale "${locale}": import fills translations, not the source. Pass --locale if the detection is wrong.`);
1192
1303
  const catalog = catalogs[locale] ??= {};
1193
1304
  for (const [key, text] of Object.entries(parsed.entries)) {
1194
1305
  if (!text.trim()) continue;
@@ -1215,7 +1326,7 @@ function parseExchangeFile(file, localeOverride) {
1215
1326
  if (/\.(xlf|xliff)$/i.test(file)) {
1216
1327
  const parsed = parseXliff(content);
1217
1328
  const locale = localeOverride ?? parsed.locale;
1218
- if (!locale) throw new Error(`[verbaly] ${file} has no trgLang/target-language pass --locale <id>.`);
1329
+ if (!locale) throw new Error(`[verbaly] ${file} has no trgLang/target-language, pass --locale <id>.`);
1219
1330
  return {
1220
1331
  locale,
1221
1332
  entries: parsed.entries
@@ -1225,7 +1336,7 @@ function parseExchangeFile(file, localeOverride) {
1225
1336
  locale: localeOverride ?? basename(file).replace(/\.csv$/i, ""),
1226
1337
  entries: parseCsv(content)
1227
1338
  };
1228
- throw new Error(`[verbaly] ${file}: unsupported format expected .xlf, .xliff or .csv.`);
1339
+ throw new Error(`[verbaly] ${file}: unsupported format, expected .xlf, .xliff or .csv.`);
1229
1340
  }
1230
1341
  function toXliff(sourceLocale, locale, entries) {
1231
1342
  const units = entries.map(({ key, source, target }) => [
@@ -1371,7 +1482,7 @@ async function loadSdk(load = () => import("@anthropic-ai/sdk")) {
1371
1482
  try {
1372
1483
  return (await load()).default;
1373
1484
  } catch (error) {
1374
- if (isModuleNotFound(error, "@anthropic-ai/sdk")) throw new Error("[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", { cause: error });
1485
+ if (isModuleNotFound(error, "@anthropic-ai/sdk")) throw new Error("[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", { cause: error });
1375
1486
  throw error;
1376
1487
  }
1377
1488
  }
@@ -1646,35 +1757,6 @@ function decodeEntities(text) {
1646
1757
  return text.replace(/&quot;/g, "\"").replace(/&#39;/g, "'").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&amp;/g, "&");
1647
1758
  }
1648
1759
  //#endregion
1649
- //#region src/transform.ts
1650
- function quote(text, single) {
1651
- if (!single) return JSON.stringify(text);
1652
- return `'${text.replace(/\\/g, "\\\\").replace(/'/g, "\\'")}'`;
1653
- }
1654
- function transformCode(code, file, analysis) {
1655
- const { tagged } = analysis ?? analyzeFile(code, file);
1656
- if (tagged.length === 0) return null;
1657
- const s = new MagicString(code);
1658
- for (const msg of tagged) {
1659
- const seen = /* @__PURE__ */ new Set();
1660
- const entries = msg.params.filter((p) => !seen.has(p.name) && seen.add(p.name));
1661
- const pairs = entries.map((p) => `${quote(p.name, msg.singleQuote)}: ${code.slice(p.start, p.end)}`).join(", ");
1662
- if (msg.jsx) {
1663
- const values = entries.length ? ` values={{ ${pairs} }}` : "";
1664
- const components = msg.jsx.components.length ? ` components={{ ${msg.jsx.components.map((c) => `${JSON.stringify(c.name)}: ${c.source}`).join(", ")} }}` : "";
1665
- s.overwrite(msg.start, msg.end, `<${msg.jsx.name} id=${JSON.stringify(msg.key)}${values}${components} />`);
1666
- continue;
1667
- }
1668
- const tagSource = code.slice(msg.tagStart, msg.tagEnd);
1669
- const args = entries.length ? `, { ${pairs} }` : "";
1670
- s.overwrite(msg.start, msg.end, `${tagSource}(${quote(msg.key, msg.singleQuote)}${args})`);
1671
- }
1672
- return {
1673
- code: s.toString(),
1674
- map: s.generateMap({ hires: true })
1675
- };
1676
- }
1677
- //#endregion
1678
- 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, translateCatalogs, writeCatalog, writeDts };
1760
+ 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, isMobileFormat, 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 };
1679
1761
 
1680
1762
  //# sourceMappingURL=index.js.map