@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/README.md +12 -1
- package/dist/{claude-mpjdqqnY.js → claude-C-ETlqsW.js} +2 -2
- package/dist/{claude-mpjdqqnY.js.map → claude-C-ETlqsW.js.map} +1 -1
- package/dist/cli.js +159 -75
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +23 -13
- package/dist/index.js +176 -94
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
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(
|
|
152
|
-
declare function writeDts(cfg: ResolvedConfig,
|
|
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
|
|
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?:
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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 ?
|
|
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}
|
|
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
|
|
511
|
-
if (types.has("number"))
|
|
512
|
-
if (types.has("string"))
|
|
513
|
-
if (types.has("date"))
|
|
514
|
-
|
|
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(
|
|
580
|
+
function generateDts(catalog) {
|
|
582
581
|
const lines = [];
|
|
583
|
-
for (const [key, message] of
|
|
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,
|
|
633
|
-
|
|
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
|
|
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
|
|
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)}
|
|
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 = (
|
|
1032
|
+
const ok = (name, message) => entries.push({
|
|
991
1033
|
level: "ok",
|
|
992
|
-
check,
|
|
1034
|
+
check: name,
|
|
993
1035
|
message
|
|
994
1036
|
});
|
|
995
|
-
const warn = (
|
|
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 = (
|
|
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
|
|
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
|
|
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(
|
|
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
|
|
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
|
|
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, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
|
1191
|
-
if (locale === cfg.sourceLocale) throw new Error(`[verbaly] ${file} targets the source locale "${locale}"
|
|
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
|
|
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
|
|
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
|
|
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(/"/g, "\"").replace(/'/g, "'").replace(/</g, "<").replace(/>/g, ">").replace(/&/g, "&");
|
|
1647
1758
|
}
|
|
1648
1759
|
//#endregion
|
|
1649
|
-
|
|
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
|