@let-value/translate-extract 1.0.30 → 1.0.31

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/src/index.js CHANGED
@@ -1,1307 +1,5 @@
1
- import { run } from "../run-C_bYec-q.js";
2
- import { isDeepStrictEqual } from "node:util";
3
- import path, { basename, dirname, extname, join, relative, resolve } from "node:path";
4
- import fs, { readFile } from "node:fs/promises";
5
- import * as gettextParser from "gettext-parser";
6
- import fs$1 from "node:fs";
7
- import Parser from "tree-sitter";
8
- import JavaScript from "tree-sitter-javascript";
9
- import TS from "tree-sitter-typescript";
10
- import { ResolverFactory } from "oxc-resolver";
11
- import { getFormula, getNPlurals } from "plural-forms";
1
+ import { cleanup, core, po, react, run } from "../run-DcO6U79B.js";
2
+ import "../static-DQHT7uqP.js";
3
+ import { defineConfig } from "../core-DR3oxhSq.js";
12
4
 
13
- //#region src/plugins/cleanup/cleanup.ts
14
- const namespace$2 = "cleanup";
15
- function cleanup() {
16
- return {
17
- name: "cleanup",
18
- setup(build) {
19
- build.context.logger?.debug("cleanup plugin initialized");
20
- const processed = /* @__PURE__ */ new Set();
21
- const generated = /* @__PURE__ */ new Set();
22
- const dirs = /* @__PURE__ */ new Set();
23
- let dispatched = false;
24
- build.onResolve({
25
- namespace: namespace$2,
26
- filter: /.*/
27
- }, ({ path: path$1 }) => {
28
- generated.add(path$1);
29
- dirs.add(dirname(path$1));
30
- Promise.all([
31
- build.defer("source"),
32
- build.defer("translate"),
33
- build.defer(namespace$2)
34
- ]).then(() => {
35
- if (dispatched) return;
36
- dispatched = true;
37
- for (const path$2 of dirs.values()) build.process({
38
- entrypoint: path$2,
39
- path: path$2,
40
- namespace: namespace$2,
41
- data: void 0
42
- });
43
- });
44
- });
45
- build.onProcess({
46
- namespace: namespace$2,
47
- filter: /.*/
48
- }, async ({ path: path$1 }) => {
49
- if (processed.has(path$1)) return;
50
- processed.add(path$1);
51
- const files = await fs.readdir(path$1).catch(() => []);
52
- for (const f of files.filter((p) => p.endsWith(".po"))) {
53
- const full = join(path$1, f);
54
- const contents = await fs.readFile(full).catch(() => void 0);
55
- if (!contents) continue;
56
- const parsed = gettextParser.po.parse(contents);
57
- const hasTranslations = Object.entries(parsed.translations || {}).some(([ctx, msgs]) => Object.keys(msgs).some((id) => !(ctx === "" && id === "")));
58
- if (!hasTranslations && generated.has(full)) await fs.unlink(full);
59
- if (hasTranslations && !generated.has(full)) build.context.logger?.warn({ path: full }, "stray translation file");
60
- }
61
- });
62
- }
63
- };
64
- }
65
-
66
- //#endregion
67
- //#region src/plugins/core/queries/comment.ts
68
- function getReference(node, { path: path$1 }) {
69
- const line = node.startPosition.row + 1;
70
- return `${relative(process.cwd(), path$1).replace(/\\+/g, "/")}:${line}`;
71
- }
72
- function getComment(node) {
73
- const text = node.text;
74
- if (text.startsWith("/*")) return text.slice(2, -2).replace(/^\s*\*?\s*/gm, "").trim();
75
- return text.replace(/^\/\/\s?/, "").trim();
76
- }
77
- const withComment = (query) => ({
78
- pattern: `(
79
- ((comment) @comment)?
80
- .
81
- (_ ${query.pattern})
82
- )`,
83
- extract(match) {
84
- const result = query.extract(match);
85
- if (!result?.translation) return result;
86
- const comment = match.captures.find((c) => c.name === "comment")?.node;
87
- if (!comment) return result;
88
- if (comment) result.translation.comments = {
89
- ...result.translation.comments,
90
- extracted: getComment(comment)
91
- };
92
- return result;
93
- }
94
- });
95
-
96
- //#endregion
97
- //#region src/plugins/core/queries/import.ts
98
- const importQuery = {
99
- pattern: `
100
- [
101
- (import_statement
102
- source: (string (string_fragment) @import))
103
- (export_statement
104
- source: (string (string_fragment) @import))
105
- (call_expression
106
- function: (identifier) @func
107
- arguments: (arguments (string (string_fragment) @import))
108
- (#eq? @func "require"))
109
- (call_expression
110
- function: (member_expression
111
- object: (identifier) @obj
112
- property: (property_identifier) @method)
113
- arguments: (arguments (string (string_fragment) @import))
114
- (#eq? @obj "require")
115
- (#eq? @method "resolve"))
116
- (call_expression
117
- function: (import)
118
- arguments: (arguments (string (string_fragment) @import)))
119
- ]
120
- `,
121
- extract(match) {
122
- return (match.captures.find((c) => c.name === "import")?.node)?.text;
123
- }
124
- };
125
-
126
- //#endregion
127
- //#region src/plugins/core/queries/utils.ts
128
- const callPattern = (fnName, args, allowMember = true) => `(
129
- (call_expression
130
- function: ${allowMember ? `[
131
- (identifier) @func
132
- (member_expression property: (property_identifier) @func)
133
- ]` : `(identifier) @func`}
134
- arguments: ${args}
135
- ) @call
136
- (#eq? @func "${fnName}")
137
- )`;
138
- function isDescendant(node, ancestor) {
139
- let current = node;
140
- while (current) {
141
- if (current.id === ancestor.id) return true;
142
- current = current.parent;
143
- }
144
- return false;
145
- }
146
-
147
- //#endregion
148
- //#region src/plugins/core/queries/message.ts
149
- const notInPlural = (query) => ({
150
- pattern: query.pattern,
151
- extract(match) {
152
- const result = query.extract(match);
153
- if (!result?.node) return result;
154
- let parent = result.node.parent;
155
- if (parent && parent.type === "arguments") parent = parent.parent;
156
- if (parent && parent.type === "call_expression") {
157
- const fn = parent.childForFieldName("function");
158
- if (fn) {
159
- if (fn.type === "identifier" && (fn.text === "plural" || fn.text === "ngettext" || fn.text === "pgettext" || fn.text === "npgettext") || fn.type === "member_expression" && [
160
- "plural",
161
- "ngettext",
162
- "pgettext",
163
- "npgettext"
164
- ].includes(fn.childForFieldName("property")?.text ?? "")) return;
165
- }
166
- }
167
- return result;
168
- }
169
- });
170
- const messageArg = `[
171
- (string (string_fragment) @msgid)
172
- (object
173
- (_)*
174
- (pair
175
- key: (property_identifier) @id_key
176
- value: (string (string_fragment) @id)
177
- (#eq? @id_key "id")
178
- )?
179
- (_)*
180
- (pair
181
- key: (property_identifier) @msg_key
182
- value: (string (string_fragment) @message)
183
- (#eq? @msg_key "message")
184
- )?
185
- (_)*
186
- )
187
- (template_string) @tpl
188
- ]`;
189
- const messageArgs = `[ (arguments ${messageArg}) (template_string) @tpl ]`;
190
- const extractMessage = (name) => (match) => {
191
- const node = match.captures.find((c) => c.name === "call")?.node;
192
- if (!node) return;
193
- const msgid = match.captures.find((c) => c.name === "msgid")?.node.text;
194
- if (msgid) return {
195
- node,
196
- translation: {
197
- id: msgid,
198
- message: [msgid]
199
- }
200
- };
201
- const tpl = match.captures.find((c) => c.name === "tpl")?.node;
202
- if (tpl) {
203
- for (const child of tpl.children) {
204
- if (child.type !== "template_substitution") continue;
205
- const expr = child.namedChildren[0];
206
- if (!expr || expr.type !== "identifier") return {
207
- node,
208
- error: `${name}() template expressions must be simple identifiers`
209
- };
210
- }
211
- const text = tpl.text.slice(1, -1);
212
- return {
213
- node,
214
- translation: {
215
- id: text,
216
- message: [text]
217
- }
218
- };
219
- }
220
- const id = match.captures.find((c) => c.name === "id")?.node.text;
221
- const message = match.captures.find((c) => c.name === "message")?.node.text;
222
- const msgId = id ?? message;
223
- if (!msgId) return;
224
- return {
225
- node,
226
- translation: {
227
- id: msgId,
228
- message: [message ?? id ?? ""]
229
- }
230
- };
231
- };
232
- const messageQuery$1 = notInPlural(withComment({
233
- pattern: callPattern("message", messageArgs),
234
- extract: extractMessage("message")
235
- }));
236
- const allowed$1 = new Set([
237
- "string",
238
- "object",
239
- "template_string"
240
- ]);
241
- const messageInvalidQuery = notInPlural({
242
- pattern: callPattern("message", "(arguments (_) @arg)"),
243
- extract(match) {
244
- const call = match.captures.find((c) => c.name === "call")?.node;
245
- const node = match.captures.find((c) => c.name === "arg")?.node;
246
- if (!call || !node) return;
247
- if (allowed$1.has(node.type)) return;
248
- return {
249
- node,
250
- error: "message() argument must be a string literal, object literal, or template literal"
251
- };
252
- }
253
- });
254
-
255
- //#endregion
256
- //#region src/plugins/core/queries/plural-utils.ts
257
- const extractPluralForms = (name) => (match) => {
258
- const call = match.captures.find((c) => c.name === "call")?.node;
259
- const n = match.captures.find((c) => c.name === "n")?.node;
260
- if (!call || !n || n.nextNamedSibling) return;
261
- const msgctxt = match.captures.find((c) => c.name === "msgctxt")?.node?.text;
262
- const msgNodes = match.captures.filter((c) => c.name === "msg").map((c) => c.node);
263
- const ids = [];
264
- const strs = [];
265
- for (const node of msgNodes) {
266
- const relevant = match.captures.filter((c) => [
267
- "msgid",
268
- "id",
269
- "message",
270
- "tpl"
271
- ].includes(c.name) && isDescendant(c.node, node));
272
- const subMatch = {
273
- pattern: 0,
274
- captures: [{
275
- name: "call",
276
- node
277
- }, ...relevant]
278
- };
279
- const result = extractMessage(name)(subMatch);
280
- if (!result) continue;
281
- if (result.error) return {
282
- node: call,
283
- error: result.error
284
- };
285
- if (result.translation) {
286
- ids.push(result.translation.id);
287
- strs.push(result.translation.message[0] ?? "");
288
- }
289
- }
290
- if (ids.length === 0) return;
291
- const translation = {
292
- id: ids[0],
293
- plural: ids[1],
294
- message: strs
295
- };
296
- if (msgctxt) translation.context = msgctxt;
297
- return {
298
- node: call,
299
- translation
300
- };
301
- };
302
-
303
- //#endregion
304
- //#region src/plugins/core/queries/context.ts
305
- const ctxCall = callPattern("context", `[
306
- (arguments (string (string_fragment) @msgctxt))
307
- (template_string) @msgctxt
308
- ]`).replace(/@call/g, "@ctx").replace(/@func/g, "@ctxfn");
309
- const contextMsgQuery = withComment({
310
- pattern: `(
311
- (call_expression
312
- function: (member_expression
313
- object: ${ctxCall}
314
- property: (property_identifier) @func
315
- )
316
- arguments: ${messageArgs}
317
- ) @call
318
- (#eq? @func "message")
319
- )`,
320
- extract(match) {
321
- const result = extractMessage("context.message")(match);
322
- const contextNode = match.captures.find((c) => c.name === "msgctxt")?.node;
323
- if (!result || !result.translation || !contextNode) return result;
324
- if (contextNode.type === "template_string") for (const child of contextNode.children) {
325
- if (child.type !== "template_substitution") continue;
326
- const expr = child.namedChildren[0];
327
- if (!expr || expr.type !== "identifier") return {
328
- node: contextNode,
329
- error: "context() template expressions must be simple identifiers"
330
- };
331
- }
332
- const contextText = contextNode.type === "template_string" ? contextNode.text.slice(1, -1) : contextNode.text;
333
- return {
334
- node: result.node,
335
- translation: {
336
- ...result.translation,
337
- context: contextText
338
- }
339
- };
340
- }
341
- });
342
- const msgCall$3 = callPattern("message", messageArgs, false).replace(/@call/g, "@msg").replace(/@func/g, "@msgfn");
343
- const contextPluralQuery = withComment({
344
- pattern: `(
345
- (call_expression
346
- function: (member_expression
347
- object: ${ctxCall}
348
- property: (property_identifier) @func
349
- )
350
- arguments: (arguments (
351
- (${msgCall$3} ("," )?)+
352
- (number) @n
353
- ))
354
- ) @call
355
- (#eq? @func "plural")
356
- )`,
357
- extract: extractPluralForms("context.plural")
358
- });
359
- const contextInvalidQuery = withComment({
360
- pattern: ctxCall,
361
- extract(match) {
362
- const call = match.captures.find((c) => c.name === "ctx")?.node;
363
- if (!call) return;
364
- const parent = call.parent;
365
- if (parent && parent.type === "member_expression" && parent.childForFieldName("object")?.id === call.id) {
366
- const property = parent.childForFieldName("property")?.text;
367
- const grandparent = parent.parent;
368
- if (grandparent && grandparent.type === "call_expression" && grandparent.childForFieldName("function")?.id === parent.id && (property === "message" || property === "plural")) return;
369
- }
370
- return {
371
- node: call,
372
- error: "context() must be used with message() or plural() in the same expression"
373
- };
374
- }
375
- });
376
-
377
- //#endregion
378
- //#region src/plugins/core/queries/gettext.ts
379
- const gettextQuery = withComment({
380
- pattern: callPattern("gettext", messageArgs),
381
- extract: extractMessage("gettext")
382
- });
383
- const allowed = new Set([
384
- "string",
385
- "object",
386
- "template_string",
387
- "identifier",
388
- "call_expression"
389
- ]);
390
- const gettextInvalidQuery = {
391
- pattern: callPattern("gettext", "(arguments (_) @arg)"),
392
- extract(match) {
393
- const call = match.captures.find((c) => c.name === "call")?.node;
394
- const node = match.captures.find((c) => c.name === "arg")?.node;
395
- if (!call || !node) return;
396
- if (allowed.has(node.type)) return;
397
- return {
398
- node,
399
- error: "gettext() argument must be a string literal, object literal, or template literal"
400
- };
401
- }
402
- };
403
-
404
- //#endregion
405
- //#region src/plugins/core/queries/ngettext.ts
406
- const msgCall$2 = callPattern("message", messageArgs).replace(/@call/g, "@msg").replace(/@func/g, "@msgfn");
407
- const plainMsg$1 = `(${messageArg}) @msg`;
408
- const msgArg$1 = `[${msgCall$2} ${plainMsg$1}]`;
409
- const ngettextQuery = withComment({
410
- pattern: callPattern("ngettext", `(arguments ${msgArg$1} "," ${msgArg$1} ("," ${msgArg$1})* "," (_) @n)`),
411
- extract: extractPluralForms("ngettext")
412
- });
413
-
414
- //#endregion
415
- //#region src/plugins/core/queries/npgettext.ts
416
- const msgCall$1 = callPattern("message", messageArgs).replace(/@call/g, "@msg").replace(/@func/g, "@msgfn");
417
- const plainMsg = `(${messageArg}) @msg`;
418
- const msgArg = `[${msgCall$1} ${plainMsg}]`;
419
- const npgettextQuery = withComment({
420
- pattern: callPattern("npgettext", `(arguments (string (string_fragment) @msgctxt) "," ${msgArg} "," ${msgArg} ("," ${msgArg})* "," (_) @n)`),
421
- extract: extractPluralForms("npgettext")
422
- });
423
-
424
- //#endregion
425
- //#region src/plugins/core/queries/pgettext.ts
426
- const pgettextQuery = withComment({
427
- pattern: callPattern("pgettext", `(arguments (string (string_fragment) @msgctxt) "," ${messageArg})`),
428
- extract(match) {
429
- const result = extractMessage("pgettext")(match);
430
- const contextNode = match.captures.find((c) => c.name === "msgctxt")?.node;
431
- if (!result || !contextNode || !result.translation) return result;
432
- return {
433
- node: result.node,
434
- translation: {
435
- ...result.translation,
436
- context: contextNode.text
437
- }
438
- };
439
- }
440
- });
441
-
442
- //#endregion
443
- //#region src/plugins/core/queries/plural.ts
444
- const msgCall = callPattern("message", messageArgs, false).replace(/@call/g, "@msg").replace(/@func/g, "@msgfn");
445
- const pluralQuery$1 = withComment({
446
- pattern: callPattern("plural", `(arguments (
447
- (${msgCall} ("," )?)+
448
- (number) @n
449
- ))`, false),
450
- extract: extractPluralForms("plural")
451
- });
452
-
453
- //#endregion
454
- //#region src/plugins/core/queries/index.ts
455
- const queries$1 = [
456
- contextMsgQuery,
457
- contextPluralQuery,
458
- contextInvalidQuery,
459
- messageQuery$1,
460
- messageInvalidQuery,
461
- gettextQuery,
462
- gettextInvalidQuery,
463
- pluralQuery$1,
464
- ngettextQuery,
465
- pgettextQuery,
466
- npgettextQuery
467
- ];
468
-
469
- //#endregion
470
- //#region src/plugins/core/parse.ts
471
- function getLanguage(ext) {
472
- switch (ext) {
473
- case ".ts": return TS.typescript;
474
- case ".tsx": return TS.tsx;
475
- default: return JavaScript;
476
- }
477
- }
478
- const parserCache = /* @__PURE__ */ new Map();
479
- const queryCache = /* @__PURE__ */ new WeakMap();
480
- function getCachedParser(ext) {
481
- let cached = parserCache.get(ext);
482
- if (!cached) {
483
- const parser = new Parser();
484
- const language = getLanguage(ext);
485
- parser.setLanguage(language);
486
- cached = {
487
- parser,
488
- language
489
- };
490
- parserCache.set(ext, cached);
491
- }
492
- return cached;
493
- }
494
- function getCachedQuery(language, pattern) {
495
- let cache = queryCache.get(language);
496
- if (!cache) {
497
- cache = /* @__PURE__ */ new Map();
498
- queryCache.set(language, cache);
499
- }
500
- let query = cache.get(pattern);
501
- if (!query) {
502
- query = new Parser.Query(language, pattern);
503
- cache.set(pattern, query);
504
- }
505
- return query;
506
- }
507
- function getParser(path$1) {
508
- const ext = extname(path$1);
509
- return getCachedParser(ext);
510
- }
511
- function getQuery(language, pattern) {
512
- return getCachedQuery(language, pattern);
513
- }
514
- function parseSource$1(source, path$1) {
515
- const context = { path: path$1 };
516
- const { parser, language } = getParser(path$1);
517
- const tree = parser.parse(source);
518
- const translations = [];
519
- const warnings = [];
520
- const imports = [];
521
- const seen = /* @__PURE__ */ new Set();
522
- for (const spec of queries$1) {
523
- const query = getCachedQuery(language, spec.pattern);
524
- for (const match of query.matches(tree.rootNode)) {
525
- const message = spec.extract(match);
526
- if (!message) continue;
527
- const { node, translation, error } = message;
528
- if (seen.has(node.id)) continue;
529
- seen.add(node.id);
530
- const reference = getReference(node, context);
531
- if (translation) translations.push({
532
- ...translation,
533
- comments: {
534
- ...translation.comments,
535
- reference
536
- }
537
- });
538
- if (error) warnings.push({
539
- error,
540
- reference
541
- });
542
- }
543
- }
544
- const importTreeQuery = getCachedQuery(language, importQuery.pattern);
545
- for (const match of importTreeQuery.matches(tree.rootNode)) {
546
- const imp = importQuery.extract(match);
547
- if (imp) imports.push(imp);
548
- }
549
- return {
550
- translations,
551
- imports,
552
- warnings
553
- };
554
- }
555
-
556
- //#endregion
557
- //#region src/plugins/core/resolve.ts
558
- function findTsconfig(dir) {
559
- let current = dir;
560
- while (true) {
561
- const config = path.join(current, "tsconfig.json");
562
- if (fs$1.existsSync(config)) return config;
563
- const parent = path.dirname(current);
564
- if (parent === current) return;
565
- current = parent;
566
- }
567
- }
568
- const resolverCache = /* @__PURE__ */ new Map();
569
- function getResolver(dir) {
570
- const tsconfig = findTsconfig(dir);
571
- const key = tsconfig ?? "__default__";
572
- let resolver = resolverCache.get(key);
573
- if (!resolver) {
574
- resolver = new ResolverFactory({
575
- extensions: [
576
- ".ts",
577
- ".tsx",
578
- ".js",
579
- ".jsx",
580
- ".mjs",
581
- ".cjs",
582
- ".json"
583
- ],
584
- conditionNames: [
585
- "import",
586
- "require",
587
- "node"
588
- ],
589
- ...tsconfig ? { tsconfig: { configFile: tsconfig } } : {}
590
- });
591
- resolverCache.set(key, resolver);
592
- }
593
- return resolver;
594
- }
595
- function resolveImports(file, imports) {
596
- const dir = path.dirname(path.resolve(file));
597
- const resolver = getResolver(dir);
598
- const resolved = [];
599
- for (const spec of imports) {
600
- const res = resolver.sync(dir, spec);
601
- if (res.path) resolved.push(res.path);
602
- }
603
- return resolved;
604
- }
605
-
606
- //#endregion
607
- //#region src/plugins/core/core.ts
608
- const filter$1 = /\.([cm]?tsx?|jsx?)$/;
609
- const namespace$1 = "source";
610
- function core() {
611
- return {
612
- name: "core",
613
- setup(build) {
614
- build.context.logger?.debug("core plugin initialized");
615
- build.onResolve({
616
- filter: filter$1,
617
- namespace: namespace$1
618
- }, ({ entrypoint, path: path$1 }) => {
619
- return {
620
- entrypoint,
621
- namespace: namespace$1,
622
- path: resolve(path$1)
623
- };
624
- });
625
- build.onLoad({
626
- filter: filter$1,
627
- namespace: namespace$1
628
- }, async ({ entrypoint, path: path$1 }) => {
629
- const data = await readFile(path$1, "utf8");
630
- return {
631
- entrypoint,
632
- path: path$1,
633
- namespace: namespace$1,
634
- data
635
- };
636
- });
637
- build.onProcess({
638
- filter: filter$1,
639
- namespace: namespace$1
640
- }, ({ entrypoint, path: path$1, data }) => {
641
- const { translations, imports, warnings } = parseSource$1(data, path$1);
642
- if (build.context.config.walk) {
643
- const paths = resolveImports(path$1, imports);
644
- for (const path$2 of paths) build.resolve({
645
- entrypoint,
646
- path: path$2,
647
- namespace: namespace$1
648
- });
649
- }
650
- for (const warning of warnings) build.context.logger?.warn(`${warning.error} at ${warning.reference}`);
651
- build.resolve({
652
- entrypoint,
653
- path: path$1,
654
- namespace: "translate",
655
- data: translations
656
- });
657
- });
658
- }
659
- };
660
- }
661
-
662
- //#endregion
663
- //#region src/plugins/po/collect.ts
664
- function collect(source, locale) {
665
- const translations = { "": {} };
666
- const nplurals = locale ? getNPlurals(locale) : void 0;
667
- for (const { context, id, message, comments, obsolete, plural } of source) {
668
- const ctx = context || "";
669
- if (!translations[ctx]) translations[ctx] = {};
670
- const length = plural ? nplurals ?? message.length : 1;
671
- const existing = translations[ctx][id];
672
- const refs = /* @__PURE__ */ new Set();
673
- if (existing?.comments?.reference) existing.comments.reference.split(/\r?\n|\r/).forEach((r) => {
674
- refs.add(r);
675
- });
676
- if (comments?.reference) comments.reference.split(/\r?\n|\r/).forEach((r) => {
677
- refs.add(r);
678
- });
679
- const msgstr = existing?.msgstr ? existing.msgstr.slice(0, length) : Array.from({ length }, () => "");
680
- while (msgstr.length < length) msgstr.push("");
681
- translations[ctx][id] = {
682
- msgctxt: context || void 0,
683
- msgid: id,
684
- msgid_plural: plural,
685
- msgstr,
686
- comments: {
687
- ...existing?.comments,
688
- ...comments,
689
- reference: refs.size ? Array.from(refs).join("\n") : void 0
690
- },
691
- obsolete: existing?.obsolete ?? obsolete
692
- };
693
- }
694
- return translations;
695
- }
696
-
697
- //#endregion
698
- //#region src/plugins/po/hasChanges.ts
699
- const IGNORED_HEADER_KEYS = new Set(["pot-creation-date", "po-revision-date"]);
700
- const IGNORED_HEADER_LINE_PREFIXES = ["pot-creation-date:", "po-revision-date:"];
701
- function normalizeHeaderString(value) {
702
- const lines = value.split("\n");
703
- const hadTrailingNewline = value.endsWith("\n");
704
- const filtered = lines.filter((line) => {
705
- const trimmed = line.trimStart().toLowerCase();
706
- return !IGNORED_HEADER_LINE_PREFIXES.some((prefix) => trimmed.startsWith(prefix));
707
- }).map((line) => {
708
- const separatorIndex = line.indexOf(":");
709
- if (separatorIndex === -1) return line;
710
- const key = line.slice(0, separatorIndex).trim().toLowerCase();
711
- const value$1 = line.slice(separatorIndex + 1);
712
- return `${key}:${value$1}`;
713
- });
714
- if (hadTrailingNewline && filtered[filtered.length - 1] !== "") filtered.push("");
715
- return filtered.join("\n");
716
- }
717
- function normalize(translations) {
718
- const compiled = gettextParser.po.compile(translations);
719
- const parsed = gettextParser.po.parse(compiled);
720
- if (parsed.headers) {
721
- const normalizedHeaders = {};
722
- for (const [key, value] of Object.entries(parsed.headers)) if (!IGNORED_HEADER_KEYS.has(key.toLowerCase())) normalizedHeaders[key.toLowerCase()] = value;
723
- parsed.headers = normalizedHeaders;
724
- }
725
- const headerMessage = parsed.translations?.[""]?.[""];
726
- if (headerMessage?.msgstr) headerMessage.msgstr = headerMessage.msgstr.map((item) => normalizeHeaderString(item));
727
- return parsed;
728
- }
729
- function hasChanges(left, right) {
730
- if (!right) return true;
731
- const normalizedLeft = normalize(left);
732
- const normalizedRight = normalize(right);
733
- return !isDeepStrictEqual(normalizedLeft, normalizedRight);
734
- }
735
-
736
- //#endregion
737
- //#region src/plugins/po/merge.ts
738
- function formatDate(date) {
739
- const pad = (n) => n.toString().padStart(2, "0");
740
- const year = date.getFullYear();
741
- const month = pad(date.getMonth() + 1);
742
- const day = pad(date.getDate());
743
- const hours = pad(date.getHours());
744
- const minutes = pad(date.getMinutes());
745
- const tzo = -date.getTimezoneOffset();
746
- const sign = tzo >= 0 ? "+" : "-";
747
- const offsetHours = pad(Math.floor(Math.abs(tzo) / 60));
748
- const offsetMinutes = pad(Math.abs(tzo) % 60);
749
- return `${year}-${month}-${day} ${hours}:${minutes}${sign}${offsetHours}${offsetMinutes}`;
750
- }
751
- function merge(sources, existing, obsolete, locale, generatedAt) {
752
- let headers = {};
753
- let translations = { "": {} };
754
- let obsoleteTranslations = {};
755
- const nplurals = getNPlurals(locale);
756
- if (existing) {
757
- headers = existing.headers ? structuredClone(existing.headers) : {};
758
- translations = existing.translations ? structuredClone(existing.translations) : { "": {} };
759
- obsoleteTranslations = existing.obsolete ? structuredClone(existing.obsolete) : {};
760
- for (const ctx of Object.keys(translations)) for (const id of Object.keys(translations[ctx])) {
761
- if (ctx === "" && id === "") continue;
762
- translations[ctx][id].obsolete = true;
763
- }
764
- }
765
- const collected = { "": {} };
766
- for (const { translations: translations$1 } of sources) for (const [ctx, msgs] of Object.entries(translations$1)) {
767
- if (!collected[ctx]) collected[ctx] = {};
768
- for (const [id, entry] of Object.entries(msgs)) {
769
- const existing$1 = collected[ctx][id];
770
- const refs = /* @__PURE__ */ new Set();
771
- if (existing$1?.comments?.reference) existing$1.comments.reference.split(/\r?\n|\r/).forEach((r) => {
772
- refs.add(r);
773
- });
774
- if (entry.comments?.reference) entry.comments.reference.split(/\r?\n|\r/).forEach((r) => {
775
- refs.add(r);
776
- });
777
- collected[ctx][id] = {
778
- ...existing$1,
779
- ...entry,
780
- comments: {
781
- ...existing$1?.comments,
782
- ...entry.comments,
783
- reference: refs.size ? Array.from(refs).join("\n") : void 0
784
- }
785
- };
786
- }
787
- }
788
- for (const [ctx, msgs] of Object.entries(collected)) {
789
- if (!translations[ctx]) translations[ctx] = {};
790
- for (const [id, entry] of Object.entries(msgs)) {
791
- const existingEntry = translations[ctx][id] ?? obsoleteTranslations[ctx]?.[id];
792
- if (existingEntry) {
793
- entry.msgstr = existingEntry.msgstr;
794
- entry.comments = {
795
- ...entry.comments,
796
- translator: existingEntry.comments?.translator
797
- };
798
- }
799
- delete entry.obsolete;
800
- entry.msgstr = entry.msgstr.slice(0, nplurals);
801
- while (entry.msgstr.length < nplurals) entry.msgstr.push("");
802
- translations[ctx][id] = entry;
803
- if (obsoleteTranslations[ctx]) delete obsoleteTranslations[ctx][id];
804
- }
805
- }
806
- for (const ctx of Object.keys(translations)) for (const id of Object.keys(translations[ctx])) {
807
- if (ctx === "" && id === "") continue;
808
- const entry = translations[ctx][id];
809
- if (entry.obsolete) {
810
- if (!obsoleteTranslations[ctx]) obsoleteTranslations[ctx] = {};
811
- obsoleteTranslations[ctx][id] = entry;
812
- delete translations[ctx][id];
813
- }
814
- }
815
- headers = {
816
- ...headers,
817
- "Content-Type": headers["Content-Type"] || "text/plain; charset=UTF-8",
818
- "Plural-Forms": `nplurals=${nplurals}; plural=${getFormula(locale)};`,
819
- language: locale,
820
- "MIME-Version": "1.0",
821
- "Content-Transfer-Encoding": "8bit",
822
- "POT-Creation-Date": formatDate(generatedAt),
823
- "x-generator": "@let-value/translate-extract"
824
- };
825
- return {
826
- charset: "utf-8",
827
- headers,
828
- translations,
829
- ...obsolete === "mark" && Object.keys(obsoleteTranslations).length ? { obsolete: obsoleteTranslations } : {}
830
- };
831
- }
832
-
833
- //#endregion
834
- //#region src/plugins/po/po.ts
835
- const namespace = "translate";
836
- function po() {
837
- return {
838
- name: "po",
839
- setup(build) {
840
- build.context.logger?.debug("po plugin initialized");
841
- const collections = /* @__PURE__ */ new Map();
842
- let dispatched = false;
843
- build.onResolve({
844
- filter: /.*/,
845
- namespace
846
- }, async ({ entrypoint, path: path$1, data }) => {
847
- if (!data || !Array.isArray(data)) return;
848
- for (const locale of build.context.config.locales) {
849
- const destination = build.context.config.destination({
850
- entrypoint,
851
- locale,
852
- path: path$1
853
- });
854
- if (!collections.has(destination)) collections.set(destination, {
855
- locale,
856
- translations: []
857
- });
858
- collections.get(destination)?.translations.push(...data);
859
- }
860
- Promise.all([build.defer("source"), build.defer(namespace)]).then(() => {
861
- if (dispatched) return;
862
- dispatched = true;
863
- for (const path$2 of collections.keys()) build.load({
864
- entrypoint,
865
- path: path$2,
866
- namespace
867
- });
868
- });
869
- });
870
- build.onLoad({
871
- filter: /.*\.po$/,
872
- namespace
873
- }, async ({ entrypoint, path: path$1 }) => {
874
- const contents = await fs.readFile(path$1).catch(() => void 0);
875
- const data = contents ? gettextParser.po.parse(contents) : void 0;
876
- return {
877
- entrypoint,
878
- path: path$1,
879
- namespace,
880
- data
881
- };
882
- });
883
- build.onProcess({
884
- filter: /.*\.po$/,
885
- namespace
886
- }, async ({ entrypoint, path: path$1, data }) => {
887
- const collected = collections.get(path$1);
888
- if (!collected) {
889
- build.context.logger?.warn({ path: path$1 }, "no translations collected for this path");
890
- return;
891
- }
892
- const { locale, translations } = collected;
893
- const record = collect(translations, locale);
894
- const out = merge([{ translations: record }], data, build.context.config.obsolete, locale, build.context.generatedAt);
895
- if (hasChanges(out, data)) {
896
- await fs.mkdir(dirname(path$1), { recursive: true });
897
- await fs.writeFile(path$1, gettextParser.po.compile(out));
898
- }
899
- build.resolve({
900
- entrypoint,
901
- path: path$1,
902
- namespace: "cleanup",
903
- data: translations
904
- });
905
- });
906
- }
907
- };
908
- }
909
-
910
- //#endregion
911
- //#region src/configuration.ts
912
- const defaultPlugins = {
913
- core,
914
- po,
915
- cleanup
916
- };
917
- const defaultDestination = ({ entrypoint, locale }) => join(dirname(entrypoint), "translations", `${basename(entrypoint, extname(entrypoint))}.${locale}.po`);
918
- const defaultExclude = [
919
- /(?:^|[\\/])node_modules(?:[\\/]|$)/,
920
- /(?:^|[\\/])dist(?:[\\/]|$)/,
921
- /(?:^|[\\/])build(?:[\\/]|$)/
922
- ];
923
- function normalizeExclude(exclude) {
924
- if (!exclude) return [];
925
- return Array.isArray(exclude) ? exclude : [exclude];
926
- }
927
- function resolveEntrypoint(ep) {
928
- if (typeof ep === "string") return { entrypoint: ep };
929
- const { entrypoint, destination, obsolete, walk, exclude } = ep;
930
- return {
931
- entrypoint,
932
- destination,
933
- obsolete,
934
- walk,
935
- exclude: exclude ? normalizeExclude(exclude) : void 0
936
- };
937
- }
938
- function resolvePlugins(user) {
939
- if (typeof user === "function") return user(defaultPlugins);
940
- if (Array.isArray(user)) return [...Object.values(defaultPlugins).map((plugin) => plugin()), ...user];
941
- return Object.values(defaultPlugins).map((plugin) => plugin());
942
- }
943
- /**
944
- * Type helper to make it easier to use translate.config.ts
945
- * @param config - {@link UserConfig}.
946
- */
947
- function defineConfig(config) {
948
- const defaultLocale = config.defaultLocale ?? "en";
949
- const plugins = resolvePlugins(config.plugins);
950
- const entrypoints = (Array.isArray(config.entrypoints) ? config.entrypoints : [config.entrypoints]).map(resolveEntrypoint);
951
- return {
952
- plugins,
953
- entrypoints,
954
- defaultLocale,
955
- locales: config.locales ?? [defaultLocale],
956
- destination: config.destination ?? defaultDestination,
957
- obsolete: config.obsolete ?? "mark",
958
- walk: config.walk ?? true,
959
- logLevel: config.logLevel ?? "info",
960
- exclude: config.exclude ? normalizeExclude(config.exclude) : defaultExclude
961
- };
962
- }
963
-
964
- //#endregion
965
- //#region src/plugins/react/queries/utils.ts
966
- function buildTemplate(node) {
967
- const source = node.tree.rootNode.text;
968
- const open = node.childForFieldName("open_tag");
969
- const close = node.childForFieldName("close_tag");
970
- const contentStart = open?.endIndex ?? node.startIndex;
971
- const contentEnd = close?.startIndex ?? node.endIndex;
972
- const parts = [];
973
- let segmentStart = contentStart;
974
- const pushRawText = (endIndex) => {
975
- if (endIndex <= segmentStart) {
976
- segmentStart = Math.max(segmentStart, endIndex);
977
- return;
978
- }
979
- const text$1 = source.slice(segmentStart, endIndex);
980
- if (text$1) parts.push({
981
- kind: "text",
982
- text: text$1,
983
- raw: true
984
- });
985
- segmentStart = endIndex;
986
- };
987
- const children = node.namedChildren.slice(1, -1);
988
- for (const child of children) if (child.type === "jsx_expression") {
989
- pushRawText(child.startIndex);
990
- const expr = child.namedChildren[0];
991
- if (!expr) return {
992
- text: "",
993
- error: "Empty JSX expression"
994
- };
995
- if (expr.type === "identifier") parts.push({
996
- kind: "expr",
997
- value: expr.text
998
- });
999
- else if (expr.type === "string") parts.push({
1000
- kind: "text",
1001
- text: expr.text.slice(1, -1),
1002
- raw: false
1003
- });
1004
- else if (expr.type === "template_string") {
1005
- if (expr.children.some((c) => c.type === "template_substitution")) return {
1006
- text: "",
1007
- error: "JSX expressions with template substitutions are not supported"
1008
- };
1009
- parts.push({
1010
- kind: "text",
1011
- text: expr.text.slice(1, -1),
1012
- raw: false
1013
- });
1014
- } else return {
1015
- text: "",
1016
- error: "JSX expressions must be simple identifiers, strings, or template literals"
1017
- };
1018
- segmentStart = child.endIndex;
1019
- } else if (child.type === "string") {
1020
- pushRawText(child.startIndex);
1021
- parts.push({
1022
- kind: "text",
1023
- text: child.text.slice(1, -1),
1024
- raw: false
1025
- });
1026
- segmentStart = child.endIndex;
1027
- } else if (child.type === "jsx_text" || child.type === "html_character_reference" || child.isError) {} else return {
1028
- text: "",
1029
- error: "Unsupported JSX child"
1030
- };
1031
- pushRawText(contentEnd);
1032
- const firstRawIndex = parts.findIndex((part) => part.kind === "text" && part.raw);
1033
- if (firstRawIndex === 0) {
1034
- const part = parts[firstRawIndex];
1035
- part.text = part.text.replace(/^\s+/, "");
1036
- }
1037
- let lastRawIndex = -1;
1038
- for (let i = parts.length - 1; i >= 0; i--) {
1039
- const part = parts[i];
1040
- if (part.kind === "text" && part.raw) {
1041
- lastRawIndex = i;
1042
- break;
1043
- }
1044
- }
1045
- if (lastRawIndex !== -1 && lastRawIndex === parts.length - 1) {
1046
- const part = parts[lastRawIndex];
1047
- part.text = part.text.replace(/\s+$/, "");
1048
- }
1049
- const strings = [""];
1050
- const values = [];
1051
- for (const part of parts) if (part.kind === "text") {
1052
- if (part.text) strings[strings.length - 1] += part.text;
1053
- } else {
1054
- values.push(part.value);
1055
- strings.push("");
1056
- }
1057
- let text = "";
1058
- for (let i = 0; i < strings.length; i++) {
1059
- text += strings[i];
1060
- if (values[i]) text += `\${${values[i]}}`;
1061
- }
1062
- return { text };
1063
- }
1064
- function buildAttrValue(node) {
1065
- if (node.type === "string") return { text: node.text.slice(1, -1) };
1066
- if (node.type === "jsx_expression") {
1067
- const expr = node.namedChildren[0];
1068
- if (!expr) return {
1069
- text: "",
1070
- error: "Empty JSX expression"
1071
- };
1072
- if (expr.type === "identifier") return { text: `\${${expr.text}}` };
1073
- else if (expr.type === "string") return { text: expr.text.slice(1, -1) };
1074
- else if (expr.type === "template_string") {
1075
- if (expr.children.some((c) => c.type === "template_substitution")) return {
1076
- text: "",
1077
- error: "JSX expressions with template substitutions are not supported"
1078
- };
1079
- return { text: expr.text.slice(1, -1) };
1080
- } else return {
1081
- text: "",
1082
- error: "JSX expressions must be simple identifiers, strings, or template literals"
1083
- };
1084
- }
1085
- return {
1086
- text: "",
1087
- error: "Unsupported JSX child"
1088
- };
1089
- }
1090
-
1091
- //#endregion
1092
- //#region src/plugins/react/queries/message.ts
1093
- const messageQuery = withComment({
1094
- pattern: `(
1095
- [
1096
- (jsx_element (jsx_opening_element name: (identifier) @name)) @call
1097
- (jsx_self_closing_element name: (identifier) @name) @call
1098
- (lexical_declaration
1099
- (variable_declarator
1100
- value: [
1101
- (jsx_element (jsx_opening_element name: (identifier) @name)) @call
1102
- (jsx_self_closing_element name: (identifier) @name) @call
1103
- ]
1104
- )
1105
- )
1106
- ]
1107
- (#eq? @name "Message")
1108
- )`,
1109
- extract(match) {
1110
- const node = match.captures.find((c) => c.name === "call")?.node;
1111
- if (!node) return void 0;
1112
- let attrs = [];
1113
- if (node.type === "jsx_element") {
1114
- const open = node.childForFieldName("open_tag");
1115
- if (open) attrs = open.namedChildren;
1116
- } else if (node.type === "jsx_self_closing_element") attrs = node.namedChildren.slice(1);
1117
- let msgctxt;
1118
- let childValue;
1119
- for (const child of attrs) {
1120
- if (child.type !== "jsx_attribute") continue;
1121
- const name = child.child(0);
1122
- const value = child.child(child.childCount - 1);
1123
- if (name?.text === "context" && value?.type === "string") msgctxt = value.text.slice(1, -1);
1124
- else if (name?.text === "children" && value) childValue = value;
1125
- }
1126
- let text = "";
1127
- let error;
1128
- if (node.type === "jsx_element") ({text, error} = buildTemplate(node));
1129
- else if (childValue) ({text, error} = buildAttrValue(childValue));
1130
- if (error) return {
1131
- node,
1132
- error
1133
- };
1134
- if (!text) return void 0;
1135
- const translation = {
1136
- id: text,
1137
- message: [text]
1138
- };
1139
- if (msgctxt) translation.context = msgctxt;
1140
- return {
1141
- node,
1142
- translation
1143
- };
1144
- }
1145
- });
1146
-
1147
- //#endregion
1148
- //#region src/plugins/react/queries/plural.ts
1149
- function parseForms(node) {
1150
- const forms = [];
1151
- if (node.type === "jsx_expression") {
1152
- const arr = node.namedChildren[0];
1153
- if (!arr || arr.type !== "array") return {
1154
- forms: [],
1155
- error: "Plural forms must be an array"
1156
- };
1157
- for (const el of arr.namedChildren) if (el.type === "jsx_element" || el.type === "jsx_fragment") {
1158
- const { text, error } = buildTemplate(el);
1159
- if (error) return {
1160
- forms: [],
1161
- error
1162
- };
1163
- forms.push(text);
1164
- } else if (el.type === "string") forms.push(el.text.slice(1, -1));
1165
- else return {
1166
- forms: [],
1167
- error: "Unsupported plural form"
1168
- };
1169
- }
1170
- return { forms };
1171
- }
1172
- const pluralQuery = withComment({
1173
- pattern: `(
1174
- [
1175
- (jsx_element (jsx_opening_element name: (identifier) @name))
1176
- (jsx_self_closing_element name: (identifier) @name)
1177
- ] @call
1178
- (#eq? @name "Plural")
1179
- )`,
1180
- extract(match) {
1181
- const node = match.captures.find((c) => c.name === "call")?.node;
1182
- if (!node) return void 0;
1183
- let attrs = [];
1184
- if (node.type === "jsx_element") {
1185
- const open = node.childForFieldName("open_tag");
1186
- if (open) attrs = open.namedChildren;
1187
- } else if (node.type === "jsx_self_closing_element") attrs = node.namedChildren.slice(1);
1188
- let msgctxt;
1189
- let formsNode;
1190
- for (const child of attrs) {
1191
- if (child.type !== "jsx_attribute") continue;
1192
- const name = child.child(0);
1193
- const value = child.child(child.childCount - 1);
1194
- if (name?.text === "context" && value?.type === "string") msgctxt = value.text.slice(1, -1);
1195
- else if (name?.text === "forms" && value) formsNode = value;
1196
- }
1197
- if (!formsNode) return void 0;
1198
- const { forms, error } = parseForms(formsNode);
1199
- if (error) return {
1200
- node,
1201
- error
1202
- };
1203
- if (forms.length === 0) return void 0;
1204
- const translation = {
1205
- id: forms[0],
1206
- plural: forms[1],
1207
- message: forms
1208
- };
1209
- if (msgctxt) translation.context = msgctxt;
1210
- return {
1211
- node,
1212
- translation
1213
- };
1214
- }
1215
- });
1216
-
1217
- //#endregion
1218
- //#region src/plugins/react/queries/index.ts
1219
- const queries = [messageQuery, pluralQuery];
1220
-
1221
- //#endregion
1222
- //#region src/plugins/react/parse.ts
1223
- function parseSource(source, path$1) {
1224
- const context = { path: path$1 };
1225
- const { parser, language } = getParser(path$1);
1226
- const tree = parser.parse(source);
1227
- const translations = [];
1228
- const warnings = [];
1229
- const seen = /* @__PURE__ */ new Set();
1230
- for (const spec of queries) {
1231
- const query = getQuery(language, spec.pattern);
1232
- for (const match of query.matches(tree.rootNode)) {
1233
- const message = spec.extract(match);
1234
- if (!message) continue;
1235
- const { node, translation, error } = message;
1236
- if (seen.has(node.id)) continue;
1237
- seen.add(node.id);
1238
- const reference = getReference(node, context);
1239
- if (translation) translations.push({
1240
- ...translation,
1241
- comments: {
1242
- ...translation.comments,
1243
- reference
1244
- }
1245
- });
1246
- if (error) warnings.push({
1247
- error,
1248
- reference
1249
- });
1250
- }
1251
- }
1252
- return {
1253
- translations,
1254
- warnings
1255
- };
1256
- }
1257
-
1258
- //#endregion
1259
- //#region src/plugins/react/react.ts
1260
- const filter = /\.[cm]?[jt]sx$/;
1261
- function react() {
1262
- return {
1263
- name: "react",
1264
- setup(build) {
1265
- build.context.logger?.debug("react plugin initialized");
1266
- build.onResolve({
1267
- filter: /.*/,
1268
- namespace: "source"
1269
- }, ({ entrypoint, path: path$1, namespace: namespace$3 }) => {
1270
- return {
1271
- entrypoint,
1272
- namespace: namespace$3,
1273
- path: resolve(path$1)
1274
- };
1275
- });
1276
- build.onLoad({
1277
- filter,
1278
- namespace: "source"
1279
- }, async ({ entrypoint, path: path$1, namespace: namespace$3 }) => {
1280
- const data = await readFile(path$1, "utf8");
1281
- return {
1282
- entrypoint,
1283
- path: path$1,
1284
- namespace: namespace$3,
1285
- data
1286
- };
1287
- });
1288
- build.onProcess({
1289
- filter,
1290
- namespace: "source"
1291
- }, ({ entrypoint, path: path$1, data }) => {
1292
- const { translations, warnings } = parseSource(data, path$1);
1293
- for (const warning of warnings) build.context.logger?.warn(`${warning.error} at ${warning.reference}`);
1294
- build.resolve({
1295
- entrypoint,
1296
- path: path$1,
1297
- namespace: "translate",
1298
- data: translations
1299
- });
1300
- });
1301
- }
1302
- };
1303
- }
1304
-
1305
- //#endregion
1306
- export { cleanup, core, defineConfig, po, react, run };
1307
- //# sourceMappingURL=index.js.map
5
+ export { cleanup, core, defineConfig, po, react, run };