@verbaly/compiler 0.25.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.
- package/dist/{claude-C-ETlqsW.js → claude-h9WA0ppg.js} +4 -2
- package/dist/claude-h9WA0ppg.js.map +1 -0
- package/dist/cli.js +452 -26
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +51 -4
- package/dist/index.js +363 -17
- package/dist/index.js.map +1 -1
- package/package.json +4 -2
- package/dist/claude-C-ETlqsW.js.map +0 -1
package/dist/index.d.ts
CHANGED
|
@@ -48,12 +48,14 @@ interface TranslateRequest {
|
|
|
48
48
|
sourceLocale: string;
|
|
49
49
|
targetLocale: string;
|
|
50
50
|
messages: Record<string, string>;
|
|
51
|
+
origins?: Record<string, string[]>;
|
|
51
52
|
}
|
|
52
53
|
type TranslateProvider = (request: TranslateRequest) => Promise<Record<string, string>>;
|
|
53
54
|
interface TranslateOptions {
|
|
54
55
|
locales?: string[];
|
|
55
56
|
batchSize?: number;
|
|
56
57
|
dryRun?: boolean;
|
|
58
|
+
origins?: Record<string, string[]>;
|
|
57
59
|
}
|
|
58
60
|
interface TranslateResult {
|
|
59
61
|
translated: Record<string, string[]>;
|
|
@@ -85,6 +87,7 @@ interface VerbalyConfig {
|
|
|
85
87
|
locales?: string[];
|
|
86
88
|
include?: string[];
|
|
87
89
|
exclude?: string[];
|
|
90
|
+
dts?: string | false;
|
|
88
91
|
translate?: TranslateConfig;
|
|
89
92
|
render?: RenderConfig;
|
|
90
93
|
}
|
|
@@ -95,6 +98,7 @@ interface ResolvedConfig {
|
|
|
95
98
|
locales: string[];
|
|
96
99
|
include: string[];
|
|
97
100
|
exclude: string[];
|
|
101
|
+
dts: string | false | undefined;
|
|
98
102
|
translate: TranslateConfig;
|
|
99
103
|
render: RenderConfig;
|
|
100
104
|
}
|
|
@@ -140,6 +144,7 @@ interface CheckResult {
|
|
|
140
144
|
}
|
|
141
145
|
declare function check(cfg: ResolvedConfig, catalogs: Catalogs, registry: MessageRegistry): CheckResult;
|
|
142
146
|
declare function formatCheckResult(result: CheckResult): string;
|
|
147
|
+
declare function githubCheckAnnotations(result: CheckResult, registry: MessageRegistry, root: string): string[];
|
|
143
148
|
//#endregion
|
|
144
149
|
//#region src/codegen.d.ts
|
|
145
150
|
declare const VIRTUAL_ID = "virtual:verbaly";
|
|
@@ -150,7 +155,7 @@ interface RuntimeModuleOptions {
|
|
|
150
155
|
declare function generateRuntimeModule(cfg: ResolvedConfig, options?: RuntimeModuleOptions): string;
|
|
151
156
|
declare function generateLocaleModule(catalog: Catalog): string;
|
|
152
157
|
declare function generateDts(catalog: Catalog): string;
|
|
153
|
-
declare function writeDts(cfg: ResolvedConfig, catalog: Catalog): void;
|
|
158
|
+
declare function writeDts(cfg: ResolvedConfig, catalog: Catalog, file?: string): void;
|
|
154
159
|
//#endregion
|
|
155
160
|
//#region src/transform.d.ts
|
|
156
161
|
interface TransformResult {
|
|
@@ -169,6 +174,7 @@ declare const SOURCE_FILE_RE: RegExp;
|
|
|
169
174
|
declare function resolveVirtualId(id: string): string | undefined;
|
|
170
175
|
declare function loadVirtualModule(id: string, cfg: ResolvedConfig, catalogs: Catalogs): string | undefined;
|
|
171
176
|
declare function isTransformTarget(id: string): boolean;
|
|
177
|
+
declare function createSourceFilter(cfg: ResolvedConfig): (id: string) => boolean;
|
|
172
178
|
declare function transformSource(code: string, id: string, registry: MessageRegistry): {
|
|
173
179
|
analysis: Analysis;
|
|
174
180
|
result: TransformResult | null;
|
|
@@ -188,6 +194,15 @@ interface DoctorResult {
|
|
|
188
194
|
}
|
|
189
195
|
declare function doctor(cfg: ResolvedConfig): Promise<DoctorResult>;
|
|
190
196
|
//#endregion
|
|
197
|
+
//#region src/drafts.d.ts
|
|
198
|
+
declare const DRAFTS_FILE = ".verbaly-drafts.json";
|
|
199
|
+
type Drafts = Record<string, string[]>;
|
|
200
|
+
declare function loadDrafts(cfg: ResolvedConfig): Drafts;
|
|
201
|
+
declare function saveDrafts(cfg: ResolvedConfig, drafts: Drafts): void;
|
|
202
|
+
declare function markDrafts(drafts: Drafts, locale: string, keys: string[]): void;
|
|
203
|
+
declare function clearDrafts(drafts: Drafts, locale: string, keys?: string[]): void;
|
|
204
|
+
declare function effectiveDrafts(drafts: Drafts, catalogs: Catalogs): Drafts;
|
|
205
|
+
//#endregion
|
|
191
206
|
//#region src/exchange.d.ts
|
|
192
207
|
type ExchangeFormat = 'xliff' | 'csv';
|
|
193
208
|
type MobileFormat = 'android-xml' | 'ios-strings';
|
|
@@ -248,7 +263,7 @@ interface InitOptions {
|
|
|
248
263
|
interface InitResult {
|
|
249
264
|
created: string[];
|
|
250
265
|
skipped: string[];
|
|
251
|
-
bundler: 'vite' | 'webpack' | 'rollup' | 'rspack' | 'esbuild' | undefined;
|
|
266
|
+
bundler: 'astro' | 'vite' | 'webpack' | 'rollup' | 'rspack' | 'esbuild' | undefined;
|
|
252
267
|
configFile: string;
|
|
253
268
|
next: string[];
|
|
254
269
|
}
|
|
@@ -319,13 +334,14 @@ interface LocaleStatus {
|
|
|
319
334
|
locale: string;
|
|
320
335
|
translated: number;
|
|
321
336
|
total: number;
|
|
337
|
+
drafts: number;
|
|
322
338
|
}
|
|
323
339
|
interface StatusResult {
|
|
324
340
|
messages: number;
|
|
325
341
|
source: string;
|
|
326
342
|
locales: LocaleStatus[];
|
|
327
343
|
}
|
|
328
|
-
declare function status(cfg: ResolvedConfig, catalogs: Catalogs, registry: MessageRegistry): StatusResult;
|
|
344
|
+
declare function status(cfg: ResolvedConfig, catalogs: Catalogs, registry: MessageRegistry, drafts?: Drafts): StatusResult;
|
|
329
345
|
declare function formatStatusResult(result: StatusResult): string;
|
|
330
346
|
//#endregion
|
|
331
347
|
//#region src/watch.d.ts
|
|
@@ -334,5 +350,36 @@ interface WatchProjectOptions {
|
|
|
334
350
|
}
|
|
335
351
|
declare function watchProject(cfg: ResolvedConfig, run: () => Promise<void>, options?: WatchProjectOptions): () => void;
|
|
336
352
|
//#endregion
|
|
337
|
-
|
|
353
|
+
//#region src/wrap.d.ts
|
|
354
|
+
interface WrapEntry {
|
|
355
|
+
file: string;
|
|
356
|
+
line: number;
|
|
357
|
+
text: string;
|
|
358
|
+
kind: 'text' | 'attribute';
|
|
359
|
+
attribute?: string;
|
|
360
|
+
}
|
|
361
|
+
interface WrapSkip {
|
|
362
|
+
file: string;
|
|
363
|
+
line: number;
|
|
364
|
+
text: string;
|
|
365
|
+
reason: string;
|
|
366
|
+
}
|
|
367
|
+
interface WrapResult {
|
|
368
|
+
files: number;
|
|
369
|
+
changed: string[];
|
|
370
|
+
wrapped: WrapEntry[];
|
|
371
|
+
skipped: WrapSkip[];
|
|
372
|
+
}
|
|
373
|
+
interface WrapOptions {
|
|
374
|
+
write?: boolean;
|
|
375
|
+
}
|
|
376
|
+
declare function wrapProject(cfg: ResolvedConfig, options?: WrapOptions): Promise<WrapResult>;
|
|
377
|
+
interface WrapCodeResult {
|
|
378
|
+
code?: string;
|
|
379
|
+
wrapped: WrapEntry[];
|
|
380
|
+
skipped: WrapSkip[];
|
|
381
|
+
}
|
|
382
|
+
declare function wrapCode(code: string, file: string): WrapCodeResult;
|
|
383
|
+
//#endregion
|
|
384
|
+
export { type Alternate, type Analysis, type AnalyzeOptions, type Catalog, type Catalogs, type CheckResult, type ClaudeProviderOptions, DRAFTS_FILE, type DoctorEntry, type DoctorResult, type Drafts, 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, type WrapCodeResult, type WrapEntry, type WrapOptions, type WrapResult, type WrapSkip, analyze, analyzeFile, analyzeSfc, catalogPath, check, claudeProvider, clearDrafts, collectParams, createSourceFilter, detectBundler, doctor, effectiveDrafts, exportCatalogs, extractProject, findConfigFile, formatCheckResult, formatStatusResult, generateDts, generateLocaleModule, generateRuntimeModule, githubCheckAnnotations, importCatalogs, init, isMobileFormat, isTransformTarget, loadCatalogs, loadConfig, loadConfigFile, loadDrafts, loadVirtualModule, markDrafts, parseExchangeFile, pruneCatalogs, pseudoCatalogs, pseudoLocalize, readCatalog, renderHtml, renderParamType, renderSite, resolveConfig, resolveVirtualId, runBuildGate, saveDrafts, serializeCatalog, stableKey, status, structureMatches, syncCatalogs, targetLocales, transformCode, transformSource, translateCatalogs, watchProject, wrapCode, wrapProject, writeCatalog, writeDts };
|
|
338
385
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
CHANGED
|
@@ -4,6 +4,7 @@ import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, watch, writeF
|
|
|
4
4
|
import { basename, dirname, join, relative, resolve } from "node:path";
|
|
5
5
|
import { RICH_TAGS, createVerbaly, localeDirection, normalizeLink, parse as parse$1, parseTags, safeAttribute } from "verbaly";
|
|
6
6
|
import { pathToFileURL } from "node:url";
|
|
7
|
+
import picomatch from "picomatch";
|
|
7
8
|
import MagicString from "magic-string";
|
|
8
9
|
import { glob } from "tinyglobby";
|
|
9
10
|
//#region src/key.ts
|
|
@@ -273,13 +274,14 @@ function isNode(value) {
|
|
|
273
274
|
}
|
|
274
275
|
//#endregion
|
|
275
276
|
//#region src/sfc.ts
|
|
276
|
-
const SFC_FILE_RE = /\.(?:svelte|vue)$/;
|
|
277
|
+
const SFC_FILE_RE = /\.(?:svelte|vue|astro)$/;
|
|
277
278
|
function analyzeFile(code, file) {
|
|
278
279
|
return SFC_FILE_RE.test(file) ? analyzeSfc(code, file) : analyze(code, file);
|
|
279
280
|
}
|
|
280
281
|
const SCRIPT_RE = /(<script\b[^>]*>)([\s\S]*?)(<\/script\s*>)/gi;
|
|
281
282
|
const STYLE_RE = /<style\b[^>]*>[\s\S]*?<\/style\s*>/gi;
|
|
282
283
|
const COMMENT_RE = /<!--[\s\S]*?-->/g;
|
|
284
|
+
const FRONTMATTER_RE = /^(\uFEFF?\s*---\r?\n)([\s\S]*?)\r?\n---(?=\r?\n|$)/;
|
|
283
285
|
const CANDIDATE_SVELTE = /(?<![\w$])\$?t(?=[`(]|\.id\()/g;
|
|
284
286
|
const CANDIDATE_VUE = /(?<![\w$])t(?=[`(]|\.id\()/g;
|
|
285
287
|
function analyzeSfc(code, file) {
|
|
@@ -287,12 +289,14 @@ function analyzeSfc(code, file) {
|
|
|
287
289
|
const options = svelte ? { tNames: ["t", "$t"] } : void 0;
|
|
288
290
|
const tagged = [];
|
|
289
291
|
const usedKeys = [];
|
|
292
|
+
const frontmatter = file.endsWith(".astro") ? FRONTMATTER_RE.exec(code) : null;
|
|
293
|
+
if (frontmatter?.[2]) merge(analyzeSegment(frontmatter[2], file, options), frontmatter[1].length, false);
|
|
290
294
|
for (const match of code.matchAll(SCRIPT_RE)) {
|
|
291
295
|
const content = match[2];
|
|
292
296
|
if (!content) continue;
|
|
293
297
|
merge(analyzeSegment(content, file, options), match.index + match[1].length, false);
|
|
294
298
|
}
|
|
295
|
-
const markup = blank(blank(blank(code, SCRIPT_RE), STYLE_RE), COMMENT_RE);
|
|
299
|
+
const markup = blank(blank(blank(frontmatter ? " ".repeat(frontmatter[0].length) + code.slice(frontmatter[0].length) : code, SCRIPT_RE), STYLE_RE), COMMENT_RE);
|
|
296
300
|
const candidates = svelte ? CANDIDATE_SVELTE : CANDIDATE_VUE;
|
|
297
301
|
let consumedTo = 0;
|
|
298
302
|
for (const match of markup.matchAll(candidates)) {
|
|
@@ -467,6 +471,57 @@ function formatCheckResult(result) {
|
|
|
467
471
|
function truncate(text, max) {
|
|
468
472
|
return text.length > max ? text.slice(0, max - 1) + "…" : text;
|
|
469
473
|
}
|
|
474
|
+
function githubCheckAnnotations(result, registry, root) {
|
|
475
|
+
const messages = registry.messages();
|
|
476
|
+
const contents = /* @__PURE__ */ new Map();
|
|
477
|
+
const readSource = (file) => {
|
|
478
|
+
if (!contents.has(file)) try {
|
|
479
|
+
contents.set(file, readFileSync(file, "utf8"));
|
|
480
|
+
} catch {
|
|
481
|
+
contents.set(file, void 0);
|
|
482
|
+
}
|
|
483
|
+
return contents.get(file);
|
|
484
|
+
};
|
|
485
|
+
const lines = [];
|
|
486
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
487
|
+
for (const entry of result.missing) {
|
|
488
|
+
const grouped = byKey.get(entry.key);
|
|
489
|
+
if (grouped) grouped.locales.push(entry.locale);
|
|
490
|
+
else byKey.set(entry.key, {
|
|
491
|
+
...entry,
|
|
492
|
+
locales: [entry.locale]
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
for (const entry of byKey.values()) {
|
|
496
|
+
const origin = messages.get(entry.key);
|
|
497
|
+
const hint = entry.source ? `: "${truncate(entry.source, 60)}"` : "";
|
|
498
|
+
const text = escapeData(`missing [${entry.locales.join(", ")}] ${entry.key}${hint}`);
|
|
499
|
+
if (origin) {
|
|
500
|
+
const file = relative(root, origin.file).replaceAll("\\", "/");
|
|
501
|
+
const content = readSource(origin.file);
|
|
502
|
+
const line = content === void 0 ? void 0 : lineAt(content, origin.start);
|
|
503
|
+
lines.push(`::error file=${escapeProperty(file)}${line ? `,line=${line}` : ""}::${text}`);
|
|
504
|
+
} else lines.push(`::error::${text}`);
|
|
505
|
+
}
|
|
506
|
+
for (const entry of result.unknown) {
|
|
507
|
+
const text = escapeData(`unknown key "${entry.key}" (not in any catalog)`);
|
|
508
|
+
const file = entry.files[0];
|
|
509
|
+
lines.push(file ? `::error file=${escapeProperty(relative(root, file).replaceAll("\\", "/"))}::${text}` : `::error::${text}`);
|
|
510
|
+
}
|
|
511
|
+
return lines;
|
|
512
|
+
}
|
|
513
|
+
function lineAt(content, offset) {
|
|
514
|
+
let line = 1;
|
|
515
|
+
const end = Math.min(offset, content.length);
|
|
516
|
+
for (let i = 0; i < end; i++) if (content[i] === "\n") line += 1;
|
|
517
|
+
return line;
|
|
518
|
+
}
|
|
519
|
+
function escapeData(text) {
|
|
520
|
+
return text.replaceAll("%", "%25").replaceAll("\r", "%0D").replaceAll("\n", "%0A");
|
|
521
|
+
}
|
|
522
|
+
function escapeProperty(text) {
|
|
523
|
+
return escapeData(text).replaceAll(":", "%3A").replaceAll(",", "%2C");
|
|
524
|
+
}
|
|
470
525
|
//#endregion
|
|
471
526
|
//#region src/params.ts
|
|
472
527
|
const PLURAL_KEYS = /* @__PURE__ */ new Set([
|
|
@@ -633,12 +688,13 @@ declare module 'virtual:verbaly/locale/*' {
|
|
|
633
688
|
}
|
|
634
689
|
`;
|
|
635
690
|
}
|
|
636
|
-
function writeDts(cfg, catalog) {
|
|
637
|
-
|
|
691
|
+
function writeDts(cfg, catalog, file) {
|
|
692
|
+
file ??= join(cfg.root, "verbaly.d.ts");
|
|
638
693
|
const content = generateDts(catalog);
|
|
639
694
|
try {
|
|
640
695
|
if (readFileSync(file, "utf8") === content) return;
|
|
641
696
|
} catch {}
|
|
697
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
642
698
|
writeFileSync(file, content);
|
|
643
699
|
}
|
|
644
700
|
//#endregion
|
|
@@ -768,15 +824,16 @@ function resolveConfig(config = {}) {
|
|
|
768
824
|
const sourceLocale = config.sourceLocale ?? "en";
|
|
769
825
|
const locales = /* @__PURE__ */ new Set([sourceLocale, ...config.locales ?? []]);
|
|
770
826
|
if (existsSync(dir)) {
|
|
771
|
-
for (const file of readdirSync(dir)) if (file.endsWith(".json") && file !== `en-XA.json`) locales.add(file.slice(0, -5));
|
|
827
|
+
for (const file of readdirSync(dir)) if (file.endsWith(".json") && !file.startsWith(".") && file !== `en-XA.json`) locales.add(file.slice(0, -5));
|
|
772
828
|
}
|
|
773
829
|
return {
|
|
774
830
|
root,
|
|
775
831
|
dir,
|
|
776
832
|
sourceLocale,
|
|
777
833
|
locales: [...locales],
|
|
778
|
-
include: config.include ?? ["{src,app}/**/*.{js,jsx,ts,tsx,mjs,mts,svelte,vue}"],
|
|
834
|
+
include: config.include ?? ["{src,app}/**/*.{js,jsx,ts,tsx,mjs,mts,svelte,vue,astro}"],
|
|
779
835
|
exclude: config.exclude ?? ["**/node_modules/**", "**/dist/**"],
|
|
836
|
+
dts: typeof config.dts === "string" ? resolve(root, config.dts) : config.dts,
|
|
780
837
|
translate: config.translate ?? {},
|
|
781
838
|
render: config.render ?? {}
|
|
782
839
|
};
|
|
@@ -863,7 +920,7 @@ function transformCode(code, file, analysis) {
|
|
|
863
920
|
//#region src/plugin.ts
|
|
864
921
|
const RESOLVED_VIRTUAL_ID = "\0virtual:verbaly";
|
|
865
922
|
const LOCALE_MODULE_PREFIX = `${RESOLVED_VIRTUAL_ID}/locale/`;
|
|
866
|
-
const SOURCE_FILE_RE = /\.(?:[cm]?[jt]sx?|svelte|vue)$/;
|
|
923
|
+
const SOURCE_FILE_RE = /\.(?:[cm]?[jt]sx?|svelte|vue|astro)$/;
|
|
867
924
|
function resolveVirtualId(id) {
|
|
868
925
|
if (id === "virtual:verbaly" || id.startsWith(`virtual:verbaly/`)) return "\0" + id;
|
|
869
926
|
}
|
|
@@ -874,6 +931,14 @@ function loadVirtualModule(id, cfg, catalogs) {
|
|
|
874
931
|
function isTransformTarget(id) {
|
|
875
932
|
return SOURCE_FILE_RE.test(id) && !id.includes("node_modules") && !id.startsWith("\0");
|
|
876
933
|
}
|
|
934
|
+
function createSourceFilter(cfg) {
|
|
935
|
+
if (cfg.include.length === 0) return () => false;
|
|
936
|
+
const matches = picomatch(cfg.include, { ignore: cfg.exclude });
|
|
937
|
+
return (id) => {
|
|
938
|
+
const rel = relative(cfg.root, id).replaceAll("\\", "/");
|
|
939
|
+
return !rel.startsWith("..") && matches(rel);
|
|
940
|
+
};
|
|
941
|
+
}
|
|
877
942
|
function transformSource(code, id, registry) {
|
|
878
943
|
const analysis = analyzeFile(code, id);
|
|
879
944
|
registry.update(id, analysis);
|
|
@@ -974,6 +1039,7 @@ function pruneCatalogs(cfg, catalogs, registry) {
|
|
|
974
1039
|
//#endregion
|
|
975
1040
|
//#region src/init.ts
|
|
976
1041
|
const BUNDLERS = [
|
|
1042
|
+
"astro",
|
|
977
1043
|
"vite",
|
|
978
1044
|
"webpack",
|
|
979
1045
|
"rollup",
|
|
@@ -1027,7 +1093,8 @@ function init(options = {}) {
|
|
|
1027
1093
|
}
|
|
1028
1094
|
const bundler = detectBundler(root);
|
|
1029
1095
|
const next = [];
|
|
1030
|
-
if (bundler === "
|
|
1096
|
+
if (bundler === "astro") next.push("install the integration: pnpm add -D @verbaly/astro", "add verbaly() to the integrations in astro.config");
|
|
1097
|
+
else if (bundler === "vite") next.push("install the plugin: pnpm add -D @verbaly/vite", "add verbaly() to the plugins in vite.config");
|
|
1031
1098
|
else if (bundler) next.push("install the plugin: pnpm add -D @verbaly/unplugin", `add the verbaly ${bundler} plugin to your build config`);
|
|
1032
1099
|
else next.push("run \"verbaly extract\" after writing your first t`…` message");
|
|
1033
1100
|
return {
|
|
@@ -1100,8 +1167,8 @@ async function doctor(cfg) {
|
|
|
1100
1167
|
else if (wired) ok("plugin", `${deps["@verbaly/vite"] ? "@verbaly/vite" : "@verbaly/unplugin"} installed for ${bundler}`);
|
|
1101
1168
|
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");
|
|
1102
1169
|
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`);
|
|
1103
|
-
if (source) {
|
|
1104
|
-
const dtsPath = join(cfg.root, "verbaly.d.ts");
|
|
1170
|
+
if (source && cfg.dts !== false) {
|
|
1171
|
+
const dtsPath = cfg.dts ?? join(cfg.root, "verbaly.d.ts");
|
|
1105
1172
|
if (!existsSync(dtsPath)) warn("types", "verbaly.d.ts has not been generated", "run `npx verbaly extract`");
|
|
1106
1173
|
else if (readFileSync(dtsPath, "utf8") !== generateDts(source)) warn("types", "verbaly.d.ts is stale", "run `npx verbaly extract`");
|
|
1107
1174
|
else ok("types", "verbaly.d.ts is up to date");
|
|
@@ -1144,6 +1211,66 @@ function readDeps(root) {
|
|
|
1144
1211
|
}
|
|
1145
1212
|
}
|
|
1146
1213
|
//#endregion
|
|
1214
|
+
//#region src/drafts.ts
|
|
1215
|
+
const DRAFTS_FILE = ".verbaly-drafts.json";
|
|
1216
|
+
function draftsPath(cfg) {
|
|
1217
|
+
return join(cfg.dir, DRAFTS_FILE);
|
|
1218
|
+
}
|
|
1219
|
+
function loadDrafts(cfg) {
|
|
1220
|
+
let content;
|
|
1221
|
+
try {
|
|
1222
|
+
content = readFileSync(draftsPath(cfg), "utf8");
|
|
1223
|
+
} catch {
|
|
1224
|
+
return {};
|
|
1225
|
+
}
|
|
1226
|
+
try {
|
|
1227
|
+
return JSON.parse(content.replace(/^\uFEFF/, ""));
|
|
1228
|
+
} catch (error) {
|
|
1229
|
+
throw new Error(`[verbaly] ${draftsPath(cfg)} is not valid JSON, fix or delete the file`, { cause: error });
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
function serializeDrafts(drafts) {
|
|
1233
|
+
const sorted = {};
|
|
1234
|
+
for (const locale of Object.keys(drafts).sort()) {
|
|
1235
|
+
const keys = [...new Set(drafts[locale])].sort();
|
|
1236
|
+
if (keys.length) sorted[locale] = keys;
|
|
1237
|
+
}
|
|
1238
|
+
return JSON.stringify(sorted, null, 2) + "\n";
|
|
1239
|
+
}
|
|
1240
|
+
function saveDrafts(cfg, drafts) {
|
|
1241
|
+
const serialized = serializeDrafts(drafts);
|
|
1242
|
+
const path = draftsPath(cfg);
|
|
1243
|
+
try {
|
|
1244
|
+
if (readFileSync(path, "utf8") === serialized) return;
|
|
1245
|
+
} catch {
|
|
1246
|
+
mkdirSync(cfg.dir, { recursive: true });
|
|
1247
|
+
}
|
|
1248
|
+
writeFileSync(path, serialized);
|
|
1249
|
+
}
|
|
1250
|
+
function markDrafts(drafts, locale, keys) {
|
|
1251
|
+
if (!keys.length) return;
|
|
1252
|
+
drafts[locale] = [.../* @__PURE__ */ new Set([...drafts[locale] ?? [], ...keys])];
|
|
1253
|
+
}
|
|
1254
|
+
function clearDrafts(drafts, locale, keys) {
|
|
1255
|
+
if (!drafts[locale]) return;
|
|
1256
|
+
if (!keys) {
|
|
1257
|
+
delete drafts[locale];
|
|
1258
|
+
return;
|
|
1259
|
+
}
|
|
1260
|
+
const drop = new Set(keys);
|
|
1261
|
+
drafts[locale] = drafts[locale].filter((key) => !drop.has(key));
|
|
1262
|
+
if (!drafts[locale].length) delete drafts[locale];
|
|
1263
|
+
}
|
|
1264
|
+
function effectiveDrafts(drafts, catalogs) {
|
|
1265
|
+
const out = {};
|
|
1266
|
+
for (const [locale, keys] of Object.entries(drafts)) {
|
|
1267
|
+
const catalog = catalogs[locale] ?? {};
|
|
1268
|
+
const live = keys.filter((key) => catalog[key]);
|
|
1269
|
+
if (live.length) out[locale] = live;
|
|
1270
|
+
}
|
|
1271
|
+
return out;
|
|
1272
|
+
}
|
|
1273
|
+
//#endregion
|
|
1147
1274
|
//#region src/mobile.ts
|
|
1148
1275
|
function androidValuesDir(locale, sourceLocale) {
|
|
1149
1276
|
if (locale === sourceLocale) return "values";
|
|
@@ -1204,10 +1331,12 @@ async function translateCatalogs(cfg, catalogs, provider, options = {}) {
|
|
|
1204
1331
|
for (let i = 0; i < missing.length; i += batchSize) {
|
|
1205
1332
|
const keys = missing.slice(i, i + batchSize);
|
|
1206
1333
|
const messages = Object.fromEntries(keys.map((key) => [key, source[key]]));
|
|
1334
|
+
const origins = options.origins ? Object.fromEntries(keys.filter((key) => options.origins[key]).map((key) => [key, options.origins[key]])) : void 0;
|
|
1207
1335
|
const out = await provider({
|
|
1208
1336
|
sourceLocale: cfg.sourceLocale,
|
|
1209
1337
|
targetLocale: locale,
|
|
1210
|
-
messages
|
|
1338
|
+
messages,
|
|
1339
|
+
origins
|
|
1211
1340
|
});
|
|
1212
1341
|
for (const key of keys) {
|
|
1213
1342
|
const text = out[key];
|
|
@@ -1484,7 +1613,9 @@ function claudeProvider(options = {}) {
|
|
|
1484
1613
|
};
|
|
1485
1614
|
}
|
|
1486
1615
|
function buildPrompt(request) {
|
|
1487
|
-
|
|
1616
|
+
const origins = request.origins;
|
|
1617
|
+
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") : "";
|
|
1618
|
+
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;
|
|
1488
1619
|
}
|
|
1489
1620
|
function batchFormat(request) {
|
|
1490
1621
|
const keys = Object.keys(request.messages);
|
|
@@ -1778,9 +1909,10 @@ function decodeEntities(text) {
|
|
|
1778
1909
|
}
|
|
1779
1910
|
//#endregion
|
|
1780
1911
|
//#region src/status.ts
|
|
1781
|
-
function status(cfg, catalogs, registry) {
|
|
1912
|
+
function status(cfg, catalogs, registry, drafts = {}) {
|
|
1782
1913
|
const source = catalogs[cfg.sourceLocale] ?? {};
|
|
1783
1914
|
const needed = /* @__PURE__ */ new Set([...registry.messages().keys(), ...Object.keys(source)]);
|
|
1915
|
+
const live = effectiveDrafts(drafts, catalogs);
|
|
1784
1916
|
const locales = [];
|
|
1785
1917
|
for (const locale of cfg.locales) {
|
|
1786
1918
|
if (locale === cfg.sourceLocale) continue;
|
|
@@ -1789,7 +1921,8 @@ function status(cfg, catalogs, registry) {
|
|
|
1789
1921
|
locales.push({
|
|
1790
1922
|
locale,
|
|
1791
1923
|
translated,
|
|
1792
|
-
total: needed.size
|
|
1924
|
+
total: needed.size,
|
|
1925
|
+
drafts: live[locale]?.length ?? 0
|
|
1793
1926
|
});
|
|
1794
1927
|
}
|
|
1795
1928
|
return {
|
|
@@ -1804,10 +1937,11 @@ function formatStatusResult(result) {
|
|
|
1804
1937
|
lines.push(" no target locales (add locales to your config)");
|
|
1805
1938
|
return lines.join("\n");
|
|
1806
1939
|
}
|
|
1807
|
-
for (const { locale, translated, total } of result.locales) {
|
|
1940
|
+
for (const { locale, translated, total, drafts } of result.locales) {
|
|
1808
1941
|
const pct = total === 0 ? 100 : Math.floor(translated / total * 100);
|
|
1809
1942
|
const mark = translated === total ? " ✓" : "";
|
|
1810
|
-
|
|
1943
|
+
const draftNote = drafts > 0 ? `, ${drafts} unreviewed` : "";
|
|
1944
|
+
lines.push(` ${locale}: ${translated}/${total} translated (${pct}%${draftNote})${mark}`);
|
|
1811
1945
|
}
|
|
1812
1946
|
return lines.join("\n");
|
|
1813
1947
|
}
|
|
@@ -1853,6 +1987,218 @@ function watchProject(cfg, run, options = {}) {
|
|
|
1853
1987
|
};
|
|
1854
1988
|
}
|
|
1855
1989
|
//#endregion
|
|
1856
|
-
|
|
1990
|
+
//#region src/wrap.ts
|
|
1991
|
+
const WRAP_ATTRS = /* @__PURE__ */ new Set([
|
|
1992
|
+
"title",
|
|
1993
|
+
"alt",
|
|
1994
|
+
"placeholder",
|
|
1995
|
+
"aria-label"
|
|
1996
|
+
]);
|
|
1997
|
+
const T_NAMES = /* @__PURE__ */ new Set(["t"]);
|
|
1998
|
+
const LETTER = /\p{L}/u;
|
|
1999
|
+
async function wrapProject(cfg, options = {}) {
|
|
2000
|
+
const files = (await glob(cfg.include, {
|
|
2001
|
+
cwd: cfg.root,
|
|
2002
|
+
ignore: cfg.exclude,
|
|
2003
|
+
absolute: true
|
|
2004
|
+
})).filter((file) => /\.[jt]sx$/.test(file));
|
|
2005
|
+
const result = {
|
|
2006
|
+
files: files.length,
|
|
2007
|
+
changed: [],
|
|
2008
|
+
wrapped: [],
|
|
2009
|
+
skipped: []
|
|
2010
|
+
};
|
|
2011
|
+
for (const file of files) {
|
|
2012
|
+
const code = readFileSync(file, "utf8");
|
|
2013
|
+
const label = relative(cfg.root, file).replaceAll("\\", "/");
|
|
2014
|
+
const out = wrapCode(code, label);
|
|
2015
|
+
result.wrapped.push(...out.wrapped);
|
|
2016
|
+
result.skipped.push(...out.skipped);
|
|
2017
|
+
if (out.code !== void 0) {
|
|
2018
|
+
result.changed.push(label);
|
|
2019
|
+
if (options.write) writeFileSync(file, out.code);
|
|
2020
|
+
}
|
|
2021
|
+
}
|
|
2022
|
+
return result;
|
|
2023
|
+
}
|
|
2024
|
+
function wrapCode(code, file) {
|
|
2025
|
+
const wrapped = [];
|
|
2026
|
+
const skipped = [];
|
|
2027
|
+
let ast;
|
|
2028
|
+
try {
|
|
2029
|
+
ast = parse(code, {
|
|
2030
|
+
sourceType: "module",
|
|
2031
|
+
errorRecovery: true,
|
|
2032
|
+
plugins: ["typescript", "jsx"]
|
|
2033
|
+
}).program;
|
|
2034
|
+
} catch {
|
|
2035
|
+
return {
|
|
2036
|
+
wrapped,
|
|
2037
|
+
skipped
|
|
2038
|
+
};
|
|
2039
|
+
}
|
|
2040
|
+
const s = new MagicString(code);
|
|
2041
|
+
const visited = /* @__PURE__ */ new Set();
|
|
2042
|
+
let changed = false;
|
|
2043
|
+
walk(ast, (node) => {
|
|
2044
|
+
if (node.type !== "JSXElement" && node.type !== "JSXFragment") return;
|
|
2045
|
+
if (visited.has(node)) return;
|
|
2046
|
+
processElement(node);
|
|
2047
|
+
});
|
|
2048
|
+
return changed ? {
|
|
2049
|
+
code: s.toString(),
|
|
2050
|
+
wrapped,
|
|
2051
|
+
skipped
|
|
2052
|
+
} : {
|
|
2053
|
+
wrapped,
|
|
2054
|
+
skipped
|
|
2055
|
+
};
|
|
2056
|
+
function processElement(node) {
|
|
2057
|
+
visited.add(node);
|
|
2058
|
+
if (node.type === "JSXElement") {
|
|
2059
|
+
const opening = node.openingElement;
|
|
2060
|
+
const name = opening.name;
|
|
2061
|
+
if (name.type === "JSXIdentifier" && name.name === "Trans") return markSubtree(node);
|
|
2062
|
+
if (hasVerbalyAttr(opening)) return markSubtree(node);
|
|
2063
|
+
wrapAttributes(opening);
|
|
2064
|
+
}
|
|
2065
|
+
const children = node.children ?? [];
|
|
2066
|
+
const hasElementChild = children.some((c) => c.type === "JSXElement" || c.type === "JSXFragment");
|
|
2067
|
+
const hasText = children.some((c) => c.type === "JSXText" && LETTER.test(c.value) || c.type === "JSXExpressionContainer" && c.expression.type === "StringLiteral" && LETTER.test(c.expression.value));
|
|
2068
|
+
if (hasElementChild && hasText) {
|
|
2069
|
+
skip(node, snippet(children), "mixed text and markup: wrap by hand or use <Trans>");
|
|
2070
|
+
return markSubtree(node);
|
|
2071
|
+
}
|
|
2072
|
+
if (hasText) {
|
|
2073
|
+
wrapSegment(node, children);
|
|
2074
|
+
return markSubtree(node);
|
|
2075
|
+
}
|
|
2076
|
+
for (const child of children) if (child.type === "JSXElement" || child.type === "JSXFragment") processElement(child);
|
|
2077
|
+
}
|
|
2078
|
+
function wrapSegment(parent, children) {
|
|
2079
|
+
const meaningful = children.filter((c) => c.type === "JSXText" && c.value.trim() !== "" || c.type === "JSXExpressionContainer" && c.expression.type !== "JSXEmptyExpression");
|
|
2080
|
+
if (meaningful.length === 0) return;
|
|
2081
|
+
const first = meaningful[0];
|
|
2082
|
+
const last = meaningful[meaningful.length - 1];
|
|
2083
|
+
let template = "";
|
|
2084
|
+
let report = "";
|
|
2085
|
+
for (let i = children.indexOf(first); i <= children.indexOf(last); i++) {
|
|
2086
|
+
const child = children[i];
|
|
2087
|
+
if (child.type === "JSXText") {
|
|
2088
|
+
let value = cleanJsxText(child.value);
|
|
2089
|
+
if (child === first) value = value.trimStart();
|
|
2090
|
+
if (child === last) value = value.trimEnd();
|
|
2091
|
+
template += escapeTemplate(value);
|
|
2092
|
+
report += value;
|
|
2093
|
+
} else if (child.type === "JSXExpressionContainer") {
|
|
2094
|
+
const expr = child.expression;
|
|
2095
|
+
if (expr.type === "JSXEmptyExpression") continue;
|
|
2096
|
+
if (usesT(expr)) {
|
|
2097
|
+
skip(parent, snippet(children), "already uses t: finish it by hand");
|
|
2098
|
+
return;
|
|
2099
|
+
}
|
|
2100
|
+
if (containsJsx(expr)) {
|
|
2101
|
+
skip(parent, snippet(children), "an expression renders markup: wrap by hand or use <Trans>");
|
|
2102
|
+
markSubtree(child);
|
|
2103
|
+
return;
|
|
2104
|
+
}
|
|
2105
|
+
if (expr.type === "StringLiteral") {
|
|
2106
|
+
template += escapeTemplate(expr.value);
|
|
2107
|
+
report += expr.value;
|
|
2108
|
+
} else {
|
|
2109
|
+
const source = code.slice(expr.start, expr.end);
|
|
2110
|
+
template += "${" + source + "}";
|
|
2111
|
+
report += "${" + source + "}";
|
|
2112
|
+
}
|
|
2113
|
+
} else return;
|
|
2114
|
+
}
|
|
2115
|
+
if (!LETTER.test(report)) return;
|
|
2116
|
+
const start = first.type === "JSXText" ? first.start + leadingWs(first.value) : first.start;
|
|
2117
|
+
const end = last.type === "JSXText" ? last.end - trailingWs(last.value) : last.end;
|
|
2118
|
+
s.overwrite(start, end, "{t`" + template + "`}");
|
|
2119
|
+
changed = true;
|
|
2120
|
+
wrapped.push({
|
|
2121
|
+
file,
|
|
2122
|
+
line: line(first),
|
|
2123
|
+
text: report,
|
|
2124
|
+
kind: "text"
|
|
2125
|
+
});
|
|
2126
|
+
}
|
|
2127
|
+
function wrapAttributes(opening) {
|
|
2128
|
+
for (const attr of opening.attributes) {
|
|
2129
|
+
if (attr.type !== "JSXAttribute") continue;
|
|
2130
|
+
const name = attr.name;
|
|
2131
|
+
if (name.type !== "JSXIdentifier" || !WRAP_ATTRS.has(name.name)) continue;
|
|
2132
|
+
const value = attr.value;
|
|
2133
|
+
if (value?.type !== "StringLiteral") continue;
|
|
2134
|
+
const raw = value.value;
|
|
2135
|
+
if (!LETTER.test(raw)) continue;
|
|
2136
|
+
s.overwrite(value.start, value.end, "{t`" + escapeTemplate(raw) + "`}");
|
|
2137
|
+
changed = true;
|
|
2138
|
+
wrapped.push({
|
|
2139
|
+
file,
|
|
2140
|
+
line: line(value),
|
|
2141
|
+
text: raw,
|
|
2142
|
+
kind: "attribute",
|
|
2143
|
+
attribute: name.name
|
|
2144
|
+
});
|
|
2145
|
+
}
|
|
2146
|
+
}
|
|
2147
|
+
function skip(node, text, reason) {
|
|
2148
|
+
skipped.push({
|
|
2149
|
+
file,
|
|
2150
|
+
line: line(node),
|
|
2151
|
+
text,
|
|
2152
|
+
reason
|
|
2153
|
+
});
|
|
2154
|
+
}
|
|
2155
|
+
function snippet(children) {
|
|
2156
|
+
const start = children[0]?.start ?? 0;
|
|
2157
|
+
const end = children[children.length - 1]?.end ?? start;
|
|
2158
|
+
const text = code.slice(start, end).replace(/\s+/g, " ").trim();
|
|
2159
|
+
return text.length > 60 ? text.slice(0, 59) + "…" : text;
|
|
2160
|
+
}
|
|
2161
|
+
function markSubtree(node) {
|
|
2162
|
+
walk(node, (n) => {
|
|
2163
|
+
if (n.type === "JSXElement" || n.type === "JSXFragment") visited.add(n);
|
|
2164
|
+
});
|
|
2165
|
+
}
|
|
2166
|
+
}
|
|
2167
|
+
function hasVerbalyAttr(opening) {
|
|
2168
|
+
return opening.attributes.some((attr) => {
|
|
2169
|
+
if (attr.type !== "JSXAttribute") return false;
|
|
2170
|
+
const name = attr.name;
|
|
2171
|
+
return name.type === "JSXIdentifier" && name.name.startsWith("data-verbaly");
|
|
2172
|
+
});
|
|
2173
|
+
}
|
|
2174
|
+
function usesT(node) {
|
|
2175
|
+
let found = false;
|
|
2176
|
+
walk(node, (n) => {
|
|
2177
|
+
if (n.type === "TaggedTemplateExpression" && isTReference(n.tag, T_NAMES)) found = true;
|
|
2178
|
+
if (n.type === "CallExpression" && isTReference(n.callee, T_NAMES)) found = true;
|
|
2179
|
+
});
|
|
2180
|
+
return found;
|
|
2181
|
+
}
|
|
2182
|
+
function containsJsx(node) {
|
|
2183
|
+
let found = false;
|
|
2184
|
+
walk(node, (n) => {
|
|
2185
|
+
if (n.type === "JSXElement" || n.type === "JSXFragment") found = true;
|
|
2186
|
+
});
|
|
2187
|
+
return found;
|
|
2188
|
+
}
|
|
2189
|
+
function escapeTemplate(text) {
|
|
2190
|
+
return text.replace(/[\\`]/g, "\\$&").replace(/\$\{/g, "\\${");
|
|
2191
|
+
}
|
|
2192
|
+
function leadingWs(text) {
|
|
2193
|
+
return text.length - text.trimStart().length;
|
|
2194
|
+
}
|
|
2195
|
+
function trailingWs(text) {
|
|
2196
|
+
return text.length - text.trimEnd().length;
|
|
2197
|
+
}
|
|
2198
|
+
function line(node) {
|
|
2199
|
+
return node.loc?.start?.line ?? 1;
|
|
2200
|
+
}
|
|
2201
|
+
//#endregion
|
|
2202
|
+
export { DRAFTS_FILE, LOCALE_MODULE_PREFIX, MessageRegistry, PSEUDO_LOCALE, RESOLVED_VIRTUAL_ID, SFC_FILE_RE, SOURCE_FILE_RE, VIRTUAL_ID, analyze, analyzeFile, analyzeSfc, catalogPath, check, claudeProvider, clearDrafts, collectParams, createSourceFilter, detectBundler, doctor, effectiveDrafts, exportCatalogs, extractProject, findConfigFile, formatCheckResult, formatStatusResult, generateDts, generateLocaleModule, generateRuntimeModule, githubCheckAnnotations, importCatalogs, init, isMobileFormat, isTransformTarget, loadCatalogs, loadConfig, loadConfigFile, loadDrafts, loadVirtualModule, markDrafts, parseExchangeFile, pruneCatalogs, pseudoCatalogs, pseudoLocalize, readCatalog, renderHtml, renderParamType, renderSite, resolveConfig, resolveVirtualId, runBuildGate, saveDrafts, serializeCatalog, stableKey, status, structureMatches, syncCatalogs, targetLocales, transformCode, transformSource, translateCatalogs, watchProject, wrapCode, wrapProject, writeCatalog, writeDts };
|
|
1857
2203
|
|
|
1858
2204
|
//# sourceMappingURL=index.js.map
|