@verbaly/compiler 0.24.0 → 0.26.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 +2 -2
- package/dist/cli.js +353 -21
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +40 -3
- package/dist/index.js +310 -15
- package/dist/index.js.map +1 -1
- package/package.json +6 -4
package/dist/index.d.ts
CHANGED
|
@@ -85,6 +85,7 @@ interface VerbalyConfig {
|
|
|
85
85
|
locales?: string[];
|
|
86
86
|
include?: string[];
|
|
87
87
|
exclude?: string[];
|
|
88
|
+
dts?: string | false;
|
|
88
89
|
translate?: TranslateConfig;
|
|
89
90
|
render?: RenderConfig;
|
|
90
91
|
}
|
|
@@ -95,6 +96,7 @@ interface ResolvedConfig {
|
|
|
95
96
|
locales: string[];
|
|
96
97
|
include: string[];
|
|
97
98
|
exclude: string[];
|
|
99
|
+
dts: string | false | undefined;
|
|
98
100
|
translate: TranslateConfig;
|
|
99
101
|
render: RenderConfig;
|
|
100
102
|
}
|
|
@@ -120,6 +122,7 @@ declare class MessageRegistry {
|
|
|
120
122
|
remove(file: string): void;
|
|
121
123
|
messages(): Map<string, TaggedMessage>;
|
|
122
124
|
usedKeys(): Map<string, string[]>;
|
|
125
|
+
origins(): Map<string, string[]>;
|
|
123
126
|
}
|
|
124
127
|
//#endregion
|
|
125
128
|
//#region src/check.d.ts
|
|
@@ -139,6 +142,7 @@ interface CheckResult {
|
|
|
139
142
|
}
|
|
140
143
|
declare function check(cfg: ResolvedConfig, catalogs: Catalogs, registry: MessageRegistry): CheckResult;
|
|
141
144
|
declare function formatCheckResult(result: CheckResult): string;
|
|
145
|
+
declare function githubCheckAnnotations(result: CheckResult, registry: MessageRegistry, root: string): string[];
|
|
142
146
|
//#endregion
|
|
143
147
|
//#region src/codegen.d.ts
|
|
144
148
|
declare const VIRTUAL_ID = "virtual:verbaly";
|
|
@@ -149,7 +153,7 @@ interface RuntimeModuleOptions {
|
|
|
149
153
|
declare function generateRuntimeModule(cfg: ResolvedConfig, options?: RuntimeModuleOptions): string;
|
|
150
154
|
declare function generateLocaleModule(catalog: Catalog): string;
|
|
151
155
|
declare function generateDts(catalog: Catalog): string;
|
|
152
|
-
declare function writeDts(cfg: ResolvedConfig, catalog: Catalog): void;
|
|
156
|
+
declare function writeDts(cfg: ResolvedConfig, catalog: Catalog, file?: string): void;
|
|
153
157
|
//#endregion
|
|
154
158
|
//#region src/transform.d.ts
|
|
155
159
|
interface TransformResult {
|
|
@@ -168,6 +172,7 @@ declare const SOURCE_FILE_RE: RegExp;
|
|
|
168
172
|
declare function resolveVirtualId(id: string): string | undefined;
|
|
169
173
|
declare function loadVirtualModule(id: string, cfg: ResolvedConfig, catalogs: Catalogs): string | undefined;
|
|
170
174
|
declare function isTransformTarget(id: string): boolean;
|
|
175
|
+
declare function createSourceFilter(cfg: ResolvedConfig): (id: string) => boolean;
|
|
171
176
|
declare function transformSource(code: string, id: string, registry: MessageRegistry): {
|
|
172
177
|
analysis: Analysis;
|
|
173
178
|
result: TransformResult | null;
|
|
@@ -197,6 +202,7 @@ interface ExportOptions {
|
|
|
197
202
|
format?: ExportFormat;
|
|
198
203
|
out?: string;
|
|
199
204
|
missing?: boolean;
|
|
205
|
+
origins?: Record<string, string[]>;
|
|
200
206
|
}
|
|
201
207
|
interface ExportedFile {
|
|
202
208
|
locale: string;
|
|
@@ -246,7 +252,7 @@ interface InitOptions {
|
|
|
246
252
|
interface InitResult {
|
|
247
253
|
created: string[];
|
|
248
254
|
skipped: string[];
|
|
249
|
-
bundler: 'vite' | 'webpack' | 'rollup' | 'rspack' | 'esbuild' | undefined;
|
|
255
|
+
bundler: 'astro' | 'vite' | 'webpack' | 'rollup' | 'rspack' | 'esbuild' | undefined;
|
|
250
256
|
configFile: string;
|
|
251
257
|
next: string[];
|
|
252
258
|
}
|
|
@@ -332,5 +338,36 @@ interface WatchProjectOptions {
|
|
|
332
338
|
}
|
|
333
339
|
declare function watchProject(cfg: ResolvedConfig, run: () => Promise<void>, options?: WatchProjectOptions): () => void;
|
|
334
340
|
//#endregion
|
|
335
|
-
|
|
341
|
+
//#region src/wrap.d.ts
|
|
342
|
+
interface WrapEntry {
|
|
343
|
+
file: string;
|
|
344
|
+
line: number;
|
|
345
|
+
text: string;
|
|
346
|
+
kind: 'text' | 'attribute';
|
|
347
|
+
attribute?: string;
|
|
348
|
+
}
|
|
349
|
+
interface WrapSkip {
|
|
350
|
+
file: string;
|
|
351
|
+
line: number;
|
|
352
|
+
text: string;
|
|
353
|
+
reason: string;
|
|
354
|
+
}
|
|
355
|
+
interface WrapResult {
|
|
356
|
+
files: number;
|
|
357
|
+
changed: string[];
|
|
358
|
+
wrapped: WrapEntry[];
|
|
359
|
+
skipped: WrapSkip[];
|
|
360
|
+
}
|
|
361
|
+
interface WrapOptions {
|
|
362
|
+
write?: boolean;
|
|
363
|
+
}
|
|
364
|
+
declare function wrapProject(cfg: ResolvedConfig, options?: WrapOptions): Promise<WrapResult>;
|
|
365
|
+
interface WrapCodeResult {
|
|
366
|
+
code?: string;
|
|
367
|
+
wrapped: WrapEntry[];
|
|
368
|
+
skipped: WrapSkip[];
|
|
369
|
+
}
|
|
370
|
+
declare function wrapCode(code: string, file: string): WrapCodeResult;
|
|
371
|
+
//#endregion
|
|
372
|
+
export { type Alternate, type Analysis, type AnalyzeOptions, type Catalog, type Catalogs, type CheckResult, type ClaudeProviderOptions, type DoctorEntry, type DoctorResult, type ExchangeFormat, type ExportFormat, type ExportOptions, type ExportResult, type ExportedFile, type ImportOptions, type ImportResult, type InitOptions, type InitResult, LOCALE_MODULE_PREFIX, type LocaleStatus, MessageRegistry, type MissingEntry, type MobileFormat, PSEUDO_LOCALE, type ParamType, type PluginOptions, RESOLVED_VIRTUAL_ID, type RenderConfig, type RenderHtmlOptions, type RenderHtmlResult, type RenderSiteOptions, type RenderSiteResult, type ResolvedConfig, type RuntimeModuleOptions, SFC_FILE_RE, SOURCE_FILE_RE, type StatusResult, type SyncResult, type TaggedMessage, type TaggedParam, type TransComponent, type TransformResult, type TranslateConfig, type TranslateOptions, type TranslateProvider, type TranslateRequest, type TranslateResult, type UnknownEntry, type UsedKey, VIRTUAL_ID, type VerbalyConfig, type WatchProjectOptions, type WrapCodeResult, type WrapEntry, type WrapOptions, type WrapResult, type WrapSkip, analyze, analyzeFile, analyzeSfc, catalogPath, check, claudeProvider, collectParams, createSourceFilter, detectBundler, doctor, exportCatalogs, extractProject, findConfigFile, formatCheckResult, formatStatusResult, generateDts, generateLocaleModule, generateRuntimeModule, githubCheckAnnotations, importCatalogs, init, isMobileFormat, isTransformTarget, loadCatalogs, loadConfig, loadConfigFile, loadVirtualModule, parseExchangeFile, pruneCatalogs, pseudoCatalogs, pseudoLocalize, readCatalog, renderHtml, renderParamType, renderSite, resolveConfig, resolveVirtualId, runBuildGate, serializeCatalog, stableKey, status, structureMatches, syncCatalogs, targetLocales, transformCode, transformSource, translateCatalogs, watchProject, wrapCode, wrapProject, writeCatalog, writeDts };
|
|
336
373
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
CHANGED
|
@@ -2,8 +2,9 @@ import { parse } from "@babel/parser";
|
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
3
|
import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, watch, writeFileSync } from "node:fs";
|
|
4
4
|
import { basename, dirname, join, relative, resolve } from "node:path";
|
|
5
|
-
import { RICH_TAGS, createVerbaly, normalizeLink, parse as parse$1, parseTags, safeAttribute } from "verbaly";
|
|
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
|
|
@@ -775,8 +831,9 @@ function resolveConfig(config = {}) {
|
|
|
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);
|
|
@@ -918,6 +983,15 @@ var MessageRegistry = class {
|
|
|
918
983
|
}
|
|
919
984
|
return out;
|
|
920
985
|
}
|
|
986
|
+
origins() {
|
|
987
|
+
const out = this.usedKeys();
|
|
988
|
+
for (const analysis of this.files.values()) for (const msg of analysis.tagged) {
|
|
989
|
+
const files = out.get(msg.key) ?? [];
|
|
990
|
+
if (!files.includes(msg.file)) files.push(msg.file);
|
|
991
|
+
out.set(msg.key, files);
|
|
992
|
+
}
|
|
993
|
+
return out;
|
|
994
|
+
}
|
|
921
995
|
};
|
|
922
996
|
//#endregion
|
|
923
997
|
//#region src/extract.ts
|
|
@@ -965,6 +1039,7 @@ function pruneCatalogs(cfg, catalogs, registry) {
|
|
|
965
1039
|
//#endregion
|
|
966
1040
|
//#region src/init.ts
|
|
967
1041
|
const BUNDLERS = [
|
|
1042
|
+
"astro",
|
|
968
1043
|
"vite",
|
|
969
1044
|
"webpack",
|
|
970
1045
|
"rollup",
|
|
@@ -1018,7 +1093,8 @@ function init(options = {}) {
|
|
|
1018
1093
|
}
|
|
1019
1094
|
const bundler = detectBundler(root);
|
|
1020
1095
|
const next = [];
|
|
1021
|
-
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");
|
|
1022
1098
|
else if (bundler) next.push("install the plugin: pnpm add -D @verbaly/unplugin", `add the verbaly ${bundler} plugin to your build config`);
|
|
1023
1099
|
else next.push("run \"verbaly extract\" after writing your first t`…` message");
|
|
1024
1100
|
return {
|
|
@@ -1091,8 +1167,8 @@ async function doctor(cfg) {
|
|
|
1091
1167
|
else if (wired) ok("plugin", `${deps["@verbaly/vite"] ? "@verbaly/vite" : "@verbaly/unplugin"} installed for ${bundler}`);
|
|
1092
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");
|
|
1093
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`);
|
|
1094
|
-
if (source) {
|
|
1095
|
-
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");
|
|
1096
1172
|
if (!existsSync(dtsPath)) warn("types", "verbaly.d.ts has not been generated", "run `npx verbaly extract`");
|
|
1097
1173
|
else if (readFileSync(dtsPath, "utf8") !== generateDts(source)) warn("types", "verbaly.d.ts is stale", "run `npx verbaly extract`");
|
|
1098
1174
|
else ok("types", "verbaly.d.ts is up to date");
|
|
@@ -1248,7 +1324,8 @@ function exportCatalogs(cfg, catalogs, options = {}) {
|
|
|
1248
1324
|
const all = Object.keys(source).filter((key) => source[key]).sort().map((key) => ({
|
|
1249
1325
|
key,
|
|
1250
1326
|
source: source[key],
|
|
1251
|
-
target: catalog[key] ?? ""
|
|
1327
|
+
target: catalog[key] ?? "",
|
|
1328
|
+
location: options.origins?.[key]
|
|
1252
1329
|
}));
|
|
1253
1330
|
const untranslated = all.filter((entry) => !entry.target);
|
|
1254
1331
|
const entries = options.missing ? untranslated : all;
|
|
@@ -1344,8 +1421,13 @@ function parseExchangeFile(file, localeOverride) {
|
|
|
1344
1421
|
throw new Error(`[verbaly] ${file}: unsupported format, expected .xlf, .xliff or .csv.`);
|
|
1345
1422
|
}
|
|
1346
1423
|
function toXliff(sourceLocale, locale, entries) {
|
|
1347
|
-
const units = entries.map(({ key, source, target }) => [
|
|
1424
|
+
const units = entries.map(({ key, source, target, location }) => [
|
|
1348
1425
|
` <unit id="${escapeXml(key)}">`,
|
|
1426
|
+
...location?.length ? [
|
|
1427
|
+
" <notes>",
|
|
1428
|
+
...location.map((file) => ` <note category="location">${escapeXml(file)}</note>`),
|
|
1429
|
+
" </notes>"
|
|
1430
|
+
] : [],
|
|
1349
1431
|
` <segment state="${target ? "translated" : "initial"}">`,
|
|
1350
1432
|
` <source>${escapeXml(source)}</source>`,
|
|
1351
1433
|
` <target>${escapeXml(target)}</target>`,
|
|
@@ -1395,8 +1477,8 @@ function unescapeXml(text) {
|
|
|
1395
1477
|
}
|
|
1396
1478
|
function toCsv(entries) {
|
|
1397
1479
|
return [
|
|
1398
|
-
"key,source,target",
|
|
1399
|
-
...entries.map(({ key, source, target }) => `${csvField(key)},${csvField(source)},${csvField(target)}`),
|
|
1480
|
+
"key,source,target,location",
|
|
1481
|
+
...entries.map(({ key, source, target, location }) => `${csvField(key)},${csvField(source)},${csvField(target)},${csvField(location?.join("; ") ?? "")}`),
|
|
1400
1482
|
""
|
|
1401
1483
|
].join("\r\n");
|
|
1402
1484
|
}
|
|
@@ -1545,6 +1627,7 @@ function renderHtml(html, options) {
|
|
|
1545
1627
|
const chunkStart = m.index + 1 + rawName.length;
|
|
1546
1628
|
if (tagName === "html" && options.setLang !== false) {
|
|
1547
1629
|
setAttribute(ms, html, chunkStart, openEnd, attrChunk, "lang", options.locale);
|
|
1630
|
+
setAttribute(ms, html, chunkStart, openEnd, attrChunk, "dir", localeDirection(options.locale));
|
|
1548
1631
|
continue;
|
|
1549
1632
|
}
|
|
1550
1633
|
const attrs = parseAttrs(attrChunk);
|
|
@@ -1837,6 +1920,218 @@ function watchProject(cfg, run, options = {}) {
|
|
|
1837
1920
|
};
|
|
1838
1921
|
}
|
|
1839
1922
|
//#endregion
|
|
1840
|
-
|
|
1923
|
+
//#region src/wrap.ts
|
|
1924
|
+
const WRAP_ATTRS = /* @__PURE__ */ new Set([
|
|
1925
|
+
"title",
|
|
1926
|
+
"alt",
|
|
1927
|
+
"placeholder",
|
|
1928
|
+
"aria-label"
|
|
1929
|
+
]);
|
|
1930
|
+
const T_NAMES = /* @__PURE__ */ new Set(["t"]);
|
|
1931
|
+
const LETTER = /\p{L}/u;
|
|
1932
|
+
async function wrapProject(cfg, options = {}) {
|
|
1933
|
+
const files = (await glob(cfg.include, {
|
|
1934
|
+
cwd: cfg.root,
|
|
1935
|
+
ignore: cfg.exclude,
|
|
1936
|
+
absolute: true
|
|
1937
|
+
})).filter((file) => /\.[jt]sx$/.test(file));
|
|
1938
|
+
const result = {
|
|
1939
|
+
files: files.length,
|
|
1940
|
+
changed: [],
|
|
1941
|
+
wrapped: [],
|
|
1942
|
+
skipped: []
|
|
1943
|
+
};
|
|
1944
|
+
for (const file of files) {
|
|
1945
|
+
const code = readFileSync(file, "utf8");
|
|
1946
|
+
const label = relative(cfg.root, file).replaceAll("\\", "/");
|
|
1947
|
+
const out = wrapCode(code, label);
|
|
1948
|
+
result.wrapped.push(...out.wrapped);
|
|
1949
|
+
result.skipped.push(...out.skipped);
|
|
1950
|
+
if (out.code !== void 0) {
|
|
1951
|
+
result.changed.push(label);
|
|
1952
|
+
if (options.write) writeFileSync(file, out.code);
|
|
1953
|
+
}
|
|
1954
|
+
}
|
|
1955
|
+
return result;
|
|
1956
|
+
}
|
|
1957
|
+
function wrapCode(code, file) {
|
|
1958
|
+
const wrapped = [];
|
|
1959
|
+
const skipped = [];
|
|
1960
|
+
let ast;
|
|
1961
|
+
try {
|
|
1962
|
+
ast = parse(code, {
|
|
1963
|
+
sourceType: "module",
|
|
1964
|
+
errorRecovery: true,
|
|
1965
|
+
plugins: ["typescript", "jsx"]
|
|
1966
|
+
}).program;
|
|
1967
|
+
} catch {
|
|
1968
|
+
return {
|
|
1969
|
+
wrapped,
|
|
1970
|
+
skipped
|
|
1971
|
+
};
|
|
1972
|
+
}
|
|
1973
|
+
const s = new MagicString(code);
|
|
1974
|
+
const visited = /* @__PURE__ */ new Set();
|
|
1975
|
+
let changed = false;
|
|
1976
|
+
walk(ast, (node) => {
|
|
1977
|
+
if (node.type !== "JSXElement" && node.type !== "JSXFragment") return;
|
|
1978
|
+
if (visited.has(node)) return;
|
|
1979
|
+
processElement(node);
|
|
1980
|
+
});
|
|
1981
|
+
return changed ? {
|
|
1982
|
+
code: s.toString(),
|
|
1983
|
+
wrapped,
|
|
1984
|
+
skipped
|
|
1985
|
+
} : {
|
|
1986
|
+
wrapped,
|
|
1987
|
+
skipped
|
|
1988
|
+
};
|
|
1989
|
+
function processElement(node) {
|
|
1990
|
+
visited.add(node);
|
|
1991
|
+
if (node.type === "JSXElement") {
|
|
1992
|
+
const opening = node.openingElement;
|
|
1993
|
+
const name = opening.name;
|
|
1994
|
+
if (name.type === "JSXIdentifier" && name.name === "Trans") return markSubtree(node);
|
|
1995
|
+
if (hasVerbalyAttr(opening)) return markSubtree(node);
|
|
1996
|
+
wrapAttributes(opening);
|
|
1997
|
+
}
|
|
1998
|
+
const children = node.children ?? [];
|
|
1999
|
+
const hasElementChild = children.some((c) => c.type === "JSXElement" || c.type === "JSXFragment");
|
|
2000
|
+
const hasText = children.some((c) => c.type === "JSXText" && LETTER.test(c.value) || c.type === "JSXExpressionContainer" && c.expression.type === "StringLiteral" && LETTER.test(c.expression.value));
|
|
2001
|
+
if (hasElementChild && hasText) {
|
|
2002
|
+
skip(node, snippet(children), "mixed text and markup: wrap by hand or use <Trans>");
|
|
2003
|
+
return markSubtree(node);
|
|
2004
|
+
}
|
|
2005
|
+
if (hasText) {
|
|
2006
|
+
wrapSegment(node, children);
|
|
2007
|
+
return markSubtree(node);
|
|
2008
|
+
}
|
|
2009
|
+
for (const child of children) if (child.type === "JSXElement" || child.type === "JSXFragment") processElement(child);
|
|
2010
|
+
}
|
|
2011
|
+
function wrapSegment(parent, children) {
|
|
2012
|
+
const meaningful = children.filter((c) => c.type === "JSXText" && c.value.trim() !== "" || c.type === "JSXExpressionContainer" && c.expression.type !== "JSXEmptyExpression");
|
|
2013
|
+
if (meaningful.length === 0) return;
|
|
2014
|
+
const first = meaningful[0];
|
|
2015
|
+
const last = meaningful[meaningful.length - 1];
|
|
2016
|
+
let template = "";
|
|
2017
|
+
let report = "";
|
|
2018
|
+
for (let i = children.indexOf(first); i <= children.indexOf(last); i++) {
|
|
2019
|
+
const child = children[i];
|
|
2020
|
+
if (child.type === "JSXText") {
|
|
2021
|
+
let value = cleanJsxText(child.value);
|
|
2022
|
+
if (child === first) value = value.trimStart();
|
|
2023
|
+
if (child === last) value = value.trimEnd();
|
|
2024
|
+
template += escapeTemplate(value);
|
|
2025
|
+
report += value;
|
|
2026
|
+
} else if (child.type === "JSXExpressionContainer") {
|
|
2027
|
+
const expr = child.expression;
|
|
2028
|
+
if (expr.type === "JSXEmptyExpression") continue;
|
|
2029
|
+
if (usesT(expr)) {
|
|
2030
|
+
skip(parent, snippet(children), "already uses t: finish it by hand");
|
|
2031
|
+
return;
|
|
2032
|
+
}
|
|
2033
|
+
if (containsJsx(expr)) {
|
|
2034
|
+
skip(parent, snippet(children), "an expression renders markup: wrap by hand or use <Trans>");
|
|
2035
|
+
markSubtree(child);
|
|
2036
|
+
return;
|
|
2037
|
+
}
|
|
2038
|
+
if (expr.type === "StringLiteral") {
|
|
2039
|
+
template += escapeTemplate(expr.value);
|
|
2040
|
+
report += expr.value;
|
|
2041
|
+
} else {
|
|
2042
|
+
const source = code.slice(expr.start, expr.end);
|
|
2043
|
+
template += "${" + source + "}";
|
|
2044
|
+
report += "${" + source + "}";
|
|
2045
|
+
}
|
|
2046
|
+
} else return;
|
|
2047
|
+
}
|
|
2048
|
+
if (!LETTER.test(report)) return;
|
|
2049
|
+
const start = first.type === "JSXText" ? first.start + leadingWs(first.value) : first.start;
|
|
2050
|
+
const end = last.type === "JSXText" ? last.end - trailingWs(last.value) : last.end;
|
|
2051
|
+
s.overwrite(start, end, "{t`" + template + "`}");
|
|
2052
|
+
changed = true;
|
|
2053
|
+
wrapped.push({
|
|
2054
|
+
file,
|
|
2055
|
+
line: line(first),
|
|
2056
|
+
text: report,
|
|
2057
|
+
kind: "text"
|
|
2058
|
+
});
|
|
2059
|
+
}
|
|
2060
|
+
function wrapAttributes(opening) {
|
|
2061
|
+
for (const attr of opening.attributes) {
|
|
2062
|
+
if (attr.type !== "JSXAttribute") continue;
|
|
2063
|
+
const name = attr.name;
|
|
2064
|
+
if (name.type !== "JSXIdentifier" || !WRAP_ATTRS.has(name.name)) continue;
|
|
2065
|
+
const value = attr.value;
|
|
2066
|
+
if (value?.type !== "StringLiteral") continue;
|
|
2067
|
+
const raw = value.value;
|
|
2068
|
+
if (!LETTER.test(raw)) continue;
|
|
2069
|
+
s.overwrite(value.start, value.end, "{t`" + escapeTemplate(raw) + "`}");
|
|
2070
|
+
changed = true;
|
|
2071
|
+
wrapped.push({
|
|
2072
|
+
file,
|
|
2073
|
+
line: line(value),
|
|
2074
|
+
text: raw,
|
|
2075
|
+
kind: "attribute",
|
|
2076
|
+
attribute: name.name
|
|
2077
|
+
});
|
|
2078
|
+
}
|
|
2079
|
+
}
|
|
2080
|
+
function skip(node, text, reason) {
|
|
2081
|
+
skipped.push({
|
|
2082
|
+
file,
|
|
2083
|
+
line: line(node),
|
|
2084
|
+
text,
|
|
2085
|
+
reason
|
|
2086
|
+
});
|
|
2087
|
+
}
|
|
2088
|
+
function snippet(children) {
|
|
2089
|
+
const start = children[0]?.start ?? 0;
|
|
2090
|
+
const end = children[children.length - 1]?.end ?? start;
|
|
2091
|
+
const text = code.slice(start, end).replace(/\s+/g, " ").trim();
|
|
2092
|
+
return text.length > 60 ? text.slice(0, 59) + "…" : text;
|
|
2093
|
+
}
|
|
2094
|
+
function markSubtree(node) {
|
|
2095
|
+
walk(node, (n) => {
|
|
2096
|
+
if (n.type === "JSXElement" || n.type === "JSXFragment") visited.add(n);
|
|
2097
|
+
});
|
|
2098
|
+
}
|
|
2099
|
+
}
|
|
2100
|
+
function hasVerbalyAttr(opening) {
|
|
2101
|
+
return opening.attributes.some((attr) => {
|
|
2102
|
+
if (attr.type !== "JSXAttribute") return false;
|
|
2103
|
+
const name = attr.name;
|
|
2104
|
+
return name.type === "JSXIdentifier" && name.name.startsWith("data-verbaly");
|
|
2105
|
+
});
|
|
2106
|
+
}
|
|
2107
|
+
function usesT(node) {
|
|
2108
|
+
let found = false;
|
|
2109
|
+
walk(node, (n) => {
|
|
2110
|
+
if (n.type === "TaggedTemplateExpression" && isTReference(n.tag, T_NAMES)) found = true;
|
|
2111
|
+
if (n.type === "CallExpression" && isTReference(n.callee, T_NAMES)) found = true;
|
|
2112
|
+
});
|
|
2113
|
+
return found;
|
|
2114
|
+
}
|
|
2115
|
+
function containsJsx(node) {
|
|
2116
|
+
let found = false;
|
|
2117
|
+
walk(node, (n) => {
|
|
2118
|
+
if (n.type === "JSXElement" || n.type === "JSXFragment") found = true;
|
|
2119
|
+
});
|
|
2120
|
+
return found;
|
|
2121
|
+
}
|
|
2122
|
+
function escapeTemplate(text) {
|
|
2123
|
+
return text.replace(/[\\`]/g, "\\$&").replace(/\$\{/g, "\\${");
|
|
2124
|
+
}
|
|
2125
|
+
function leadingWs(text) {
|
|
2126
|
+
return text.length - text.trimStart().length;
|
|
2127
|
+
}
|
|
2128
|
+
function trailingWs(text) {
|
|
2129
|
+
return text.length - text.trimEnd().length;
|
|
2130
|
+
}
|
|
2131
|
+
function line(node) {
|
|
2132
|
+
return node.loc?.start?.line ?? 1;
|
|
2133
|
+
}
|
|
2134
|
+
//#endregion
|
|
2135
|
+
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, createSourceFilter, detectBundler, doctor, exportCatalogs, extractProject, findConfigFile, formatCheckResult, formatStatusResult, generateDts, generateLocaleModule, generateRuntimeModule, githubCheckAnnotations, importCatalogs, init, isMobileFormat, isTransformTarget, loadCatalogs, loadConfig, loadConfigFile, loadVirtualModule, parseExchangeFile, pruneCatalogs, pseudoCatalogs, pseudoLocalize, readCatalog, renderHtml, renderParamType, renderSite, resolveConfig, resolveVirtualId, runBuildGate, serializeCatalog, stableKey, status, structureMatches, syncCatalogs, targetLocales, transformCode, transformSource, translateCatalogs, watchProject, wrapCode, wrapProject, writeCatalog, writeDts };
|
|
1841
2136
|
|
|
1842
2137
|
//# sourceMappingURL=index.js.map
|