@let-value/translate-extract 1.0.6-beta.2
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/bin/cli.cjs +19 -0
- package/dist/bin/cli.d.cts +2 -0
- package/dist/bin/cli.d.ts +2 -0
- package/dist/bin/cli.js +19 -0
- package/dist/bin/cli.js.map +1 -0
- package/dist/chunk-CUT6urMc.cjs +30 -0
- package/dist/src/index.cjs +721 -0
- package/dist/src/index.d.cts +94 -0
- package/dist/src/index.d.cts.map +1 -0
- package/dist/src/index.d.ts +94 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +710 -0
- package/dist/src/index.js.map +1 -0
- package/package.json +45 -0
|
@@ -0,0 +1,710 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path, { basename, dirname, extname, join, relative, resolve } from "node:path";
|
|
3
|
+
import { globSync } from "glob";
|
|
4
|
+
import fs$1, { readFile } from "node:fs/promises";
|
|
5
|
+
import { assign, memo } from "radash";
|
|
6
|
+
import Parser from "tree-sitter";
|
|
7
|
+
import JavaScript from "tree-sitter-javascript";
|
|
8
|
+
import TS from "tree-sitter-typescript";
|
|
9
|
+
import { ResolverFactory } from "oxc-resolver";
|
|
10
|
+
import * as gettextParser from "gettext-parser";
|
|
11
|
+
import { getFormula, getNPlurals } from "plural-forms";
|
|
12
|
+
|
|
13
|
+
//#region src/plugins/core/queries/comment.ts
|
|
14
|
+
function getReference(node, { path: path$1 }) {
|
|
15
|
+
const line = node.startPosition.row + 1;
|
|
16
|
+
const col = node.startPosition.column + 1;
|
|
17
|
+
const rel = relative(process.cwd(), path$1);
|
|
18
|
+
return `${rel}:${line}:${col}`;
|
|
19
|
+
}
|
|
20
|
+
function getComment(node) {
|
|
21
|
+
const text = node.text;
|
|
22
|
+
if (text.startsWith("/*")) return text.slice(2, -2).replace(/^\s*\*?\s*/gm, "").trim();
|
|
23
|
+
return text.replace(/^\/\/\s?/, "").trim();
|
|
24
|
+
}
|
|
25
|
+
const withComment = (query) => ({
|
|
26
|
+
pattern: `(
|
|
27
|
+
((comment) @comment)?
|
|
28
|
+
.
|
|
29
|
+
(_ ${query.pattern})
|
|
30
|
+
)`,
|
|
31
|
+
extract(match) {
|
|
32
|
+
const result = query.extract(match);
|
|
33
|
+
if (!result?.translation) return result;
|
|
34
|
+
const comment = match.captures.find((c) => c.name === "comment")?.node;
|
|
35
|
+
if (!comment) return result;
|
|
36
|
+
if (comment) result.translation.comments = {
|
|
37
|
+
...result.translation.comments,
|
|
38
|
+
extracted: getComment(comment)
|
|
39
|
+
};
|
|
40
|
+
return result;
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
//#endregion
|
|
45
|
+
//#region src/plugins/core/queries/import.ts
|
|
46
|
+
const importQuery = {
|
|
47
|
+
pattern: `
|
|
48
|
+
[
|
|
49
|
+
(import_statement
|
|
50
|
+
source: (string (string_fragment) @import))
|
|
51
|
+
(export_statement
|
|
52
|
+
source: (string (string_fragment) @import))
|
|
53
|
+
(call_expression
|
|
54
|
+
function: (identifier) @func
|
|
55
|
+
arguments: (arguments (string (string_fragment) @import))
|
|
56
|
+
(#eq? @func "require"))
|
|
57
|
+
(call_expression
|
|
58
|
+
function: (member_expression
|
|
59
|
+
object: (identifier) @obj
|
|
60
|
+
property: (property_identifier) @method)
|
|
61
|
+
arguments: (arguments (string (string_fragment) @import))
|
|
62
|
+
(#eq? @obj "require")
|
|
63
|
+
(#eq? @method "resolve"))
|
|
64
|
+
(call_expression
|
|
65
|
+
function: (import)
|
|
66
|
+
arguments: (arguments (string (string_fragment) @import)))
|
|
67
|
+
]
|
|
68
|
+
`,
|
|
69
|
+
extract(match) {
|
|
70
|
+
const node = match.captures.find((c) => c.name === "import")?.node;
|
|
71
|
+
return node?.text;
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
//#endregion
|
|
76
|
+
//#region src/plugins/core/queries/utils.ts
|
|
77
|
+
const callPattern = (fnName, args, allowMember = true) => `(
|
|
78
|
+
(call_expression
|
|
79
|
+
function: ${allowMember ? `[
|
|
80
|
+
(identifier) @func
|
|
81
|
+
(member_expression property: (property_identifier) @func)
|
|
82
|
+
]` : `(identifier) @func`}
|
|
83
|
+
arguments: ${args}
|
|
84
|
+
) @call
|
|
85
|
+
(#eq? @func "${fnName}")
|
|
86
|
+
)`;
|
|
87
|
+
function isDescendant(node, ancestor) {
|
|
88
|
+
let current = node;
|
|
89
|
+
while (current) {
|
|
90
|
+
if (current.id === ancestor.id) return true;
|
|
91
|
+
current = current.parent;
|
|
92
|
+
}
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
//#endregion
|
|
97
|
+
//#region src/plugins/core/queries/message.ts
|
|
98
|
+
const notInPlural = (query) => ({
|
|
99
|
+
pattern: query.pattern,
|
|
100
|
+
extract(match) {
|
|
101
|
+
const result = query.extract(match);
|
|
102
|
+
if (!result?.node) return result;
|
|
103
|
+
let parent = result.node.parent;
|
|
104
|
+
if (parent && parent.type === "arguments") parent = parent.parent;
|
|
105
|
+
if (parent && parent.type === "call_expression") {
|
|
106
|
+
const fn = parent.childForFieldName("function");
|
|
107
|
+
if (fn) {
|
|
108
|
+
if (fn.type === "identifier" && (fn.text === "plural" || fn.text === "ngettext" || fn.text === "pgettext" || fn.text === "npgettext") || fn.type === "member_expression" && [
|
|
109
|
+
"plural",
|
|
110
|
+
"ngettext",
|
|
111
|
+
"pgettext",
|
|
112
|
+
"npgettext"
|
|
113
|
+
].includes(fn.childForFieldName("property")?.text ?? "")) return void 0;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return result;
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
const messageArg = `[
|
|
120
|
+
(string (string_fragment) @msgid)
|
|
121
|
+
(object
|
|
122
|
+
(_)*
|
|
123
|
+
(pair
|
|
124
|
+
key: (property_identifier) @id_key
|
|
125
|
+
value: (string (string_fragment) @id)
|
|
126
|
+
(#eq? @id_key "id")
|
|
127
|
+
)?
|
|
128
|
+
(_)*
|
|
129
|
+
(pair
|
|
130
|
+
key: (property_identifier) @msg_key
|
|
131
|
+
value: (string (string_fragment) @message)
|
|
132
|
+
(#eq? @msg_key "message")
|
|
133
|
+
)?
|
|
134
|
+
(_)*
|
|
135
|
+
)
|
|
136
|
+
(template_string) @tpl
|
|
137
|
+
]`;
|
|
138
|
+
const messageArgs = `[ (arguments ${messageArg}) (template_string) @tpl ]`;
|
|
139
|
+
const extractMessage = (name) => (match) => {
|
|
140
|
+
const node = match.captures.find((c) => c.name === "call")?.node;
|
|
141
|
+
if (!node) return void 0;
|
|
142
|
+
const msgid = match.captures.find((c) => c.name === "msgid")?.node.text;
|
|
143
|
+
if (msgid) return {
|
|
144
|
+
node,
|
|
145
|
+
translation: {
|
|
146
|
+
id: msgid,
|
|
147
|
+
message: [msgid]
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
const tpl = match.captures.find((c) => c.name === "tpl")?.node;
|
|
151
|
+
if (tpl) {
|
|
152
|
+
for (const child of tpl.children) {
|
|
153
|
+
if (child.type !== "template_substitution") continue;
|
|
154
|
+
const expr = child.namedChildren[0];
|
|
155
|
+
if (!expr || expr.type !== "identifier") return {
|
|
156
|
+
node,
|
|
157
|
+
error: `${name}() template expressions must be simple identifiers`
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
const text = tpl.text.slice(1, -1);
|
|
161
|
+
return {
|
|
162
|
+
node,
|
|
163
|
+
translation: {
|
|
164
|
+
id: text,
|
|
165
|
+
message: [text]
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
const id = match.captures.find((c) => c.name === "id")?.node.text;
|
|
170
|
+
const message = match.captures.find((c) => c.name === "message")?.node.text;
|
|
171
|
+
const msgId = id ?? message;
|
|
172
|
+
if (!msgId) return void 0;
|
|
173
|
+
const msgstr = message ?? id ?? "";
|
|
174
|
+
return {
|
|
175
|
+
node,
|
|
176
|
+
translation: {
|
|
177
|
+
id: msgId,
|
|
178
|
+
message: [msgstr]
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
};
|
|
182
|
+
const messageQuery = notInPlural(withComment({
|
|
183
|
+
pattern: callPattern("message", messageArgs),
|
|
184
|
+
extract: extractMessage("message")
|
|
185
|
+
}));
|
|
186
|
+
const allowed$1 = new Set([
|
|
187
|
+
"string",
|
|
188
|
+
"object",
|
|
189
|
+
"template_string"
|
|
190
|
+
]);
|
|
191
|
+
const messageInvalidQuery = notInPlural({
|
|
192
|
+
pattern: callPattern("message", "(arguments (_) @arg)"),
|
|
193
|
+
extract(match) {
|
|
194
|
+
const call = match.captures.find((c) => c.name === "call")?.node;
|
|
195
|
+
const node = match.captures.find((c) => c.name === "arg")?.node;
|
|
196
|
+
if (!call || !node) return void 0;
|
|
197
|
+
if (allowed$1.has(node.type)) return void 0;
|
|
198
|
+
return {
|
|
199
|
+
node,
|
|
200
|
+
error: "message() argument must be a string literal, object literal, or template literal"
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
//#endregion
|
|
206
|
+
//#region src/plugins/core/queries/plural-utils.ts
|
|
207
|
+
const extractPluralForms = (name) => (match) => {
|
|
208
|
+
const call = match.captures.find((c) => c.name === "call")?.node;
|
|
209
|
+
const n = match.captures.find((c) => c.name === "n")?.node;
|
|
210
|
+
if (!call || !n || n.nextNamedSibling) return void 0;
|
|
211
|
+
const msgctxt = match.captures.find((c) => c.name === "msgctxt")?.node?.text;
|
|
212
|
+
const msgNodes = match.captures.filter((c) => c.name === "msg").map((c) => c.node);
|
|
213
|
+
const ids = [];
|
|
214
|
+
const strs = [];
|
|
215
|
+
for (const node of msgNodes) {
|
|
216
|
+
const relevant = match.captures.filter((c) => [
|
|
217
|
+
"msgid",
|
|
218
|
+
"id",
|
|
219
|
+
"message",
|
|
220
|
+
"tpl"
|
|
221
|
+
].includes(c.name) && isDescendant(c.node, node));
|
|
222
|
+
const subMatch = {
|
|
223
|
+
pattern: 0,
|
|
224
|
+
captures: [{
|
|
225
|
+
name: "call",
|
|
226
|
+
node
|
|
227
|
+
}, ...relevant]
|
|
228
|
+
};
|
|
229
|
+
const result = extractMessage(name)(subMatch);
|
|
230
|
+
if (!result) continue;
|
|
231
|
+
if (result.error) return {
|
|
232
|
+
node: call,
|
|
233
|
+
error: result.error
|
|
234
|
+
};
|
|
235
|
+
if (result.translation) {
|
|
236
|
+
ids.push(result.translation.id);
|
|
237
|
+
strs.push(result.translation.message[0] ?? "");
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
if (ids.length === 0) return void 0;
|
|
241
|
+
const translation = {
|
|
242
|
+
id: ids[0],
|
|
243
|
+
plural: ids[1],
|
|
244
|
+
message: strs
|
|
245
|
+
};
|
|
246
|
+
if (msgctxt) translation.context = msgctxt;
|
|
247
|
+
return {
|
|
248
|
+
node: call,
|
|
249
|
+
translation
|
|
250
|
+
};
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
//#endregion
|
|
254
|
+
//#region src/plugins/core/queries/context.ts
|
|
255
|
+
const ctxCall = callPattern("context", `(arguments (string (string_fragment) @msgctxt))`).replace(/@call/g, "@ctx").replace(/@func/g, "@ctxfn");
|
|
256
|
+
const contextMsgQuery = withComment({
|
|
257
|
+
pattern: `(
|
|
258
|
+
(call_expression
|
|
259
|
+
function: (member_expression
|
|
260
|
+
object: ${ctxCall}
|
|
261
|
+
property: (property_identifier) @func
|
|
262
|
+
)
|
|
263
|
+
arguments: ${messageArgs}
|
|
264
|
+
) @call
|
|
265
|
+
(#eq? @func "message")
|
|
266
|
+
)`,
|
|
267
|
+
extract(match) {
|
|
268
|
+
const result = extractMessage("context.message")(match);
|
|
269
|
+
const contextNode = match.captures.find((c) => c.name === "msgctxt")?.node;
|
|
270
|
+
if (!result || !result.translation || !contextNode) return result;
|
|
271
|
+
return {
|
|
272
|
+
node: result.node,
|
|
273
|
+
translation: {
|
|
274
|
+
...result.translation,
|
|
275
|
+
context: contextNode.text
|
|
276
|
+
}
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
});
|
|
280
|
+
const msgCall$3 = callPattern("message", messageArgs, false).replace(/@call/g, "@msg").replace(/@func/g, "@msgfn");
|
|
281
|
+
const contextPluralQuery = withComment({
|
|
282
|
+
pattern: `(
|
|
283
|
+
(call_expression
|
|
284
|
+
function: (member_expression
|
|
285
|
+
object: ${ctxCall}
|
|
286
|
+
property: (property_identifier) @func
|
|
287
|
+
)
|
|
288
|
+
arguments: (arguments (
|
|
289
|
+
(${msgCall$3} ("," )?)+
|
|
290
|
+
(number) @n
|
|
291
|
+
))
|
|
292
|
+
) @call
|
|
293
|
+
(#eq? @func "plural")
|
|
294
|
+
)`,
|
|
295
|
+
extract: extractPluralForms("context.plural")
|
|
296
|
+
});
|
|
297
|
+
const contextInvalidQuery = withComment({
|
|
298
|
+
pattern: ctxCall,
|
|
299
|
+
extract(match) {
|
|
300
|
+
const call = match.captures.find((c) => c.name === "ctx")?.node;
|
|
301
|
+
if (!call) return void 0;
|
|
302
|
+
const parent = call.parent;
|
|
303
|
+
if (parent && parent.type === "member_expression" && parent.childForFieldName("object")?.id === call.id) {
|
|
304
|
+
const property = parent.childForFieldName("property")?.text;
|
|
305
|
+
const grandparent = parent.parent;
|
|
306
|
+
if (grandparent && grandparent.type === "call_expression" && grandparent.childForFieldName("function")?.id === parent.id && (property === "message" || property === "plural")) return void 0;
|
|
307
|
+
}
|
|
308
|
+
return {
|
|
309
|
+
node: call,
|
|
310
|
+
error: "context() must be used with message() or plural() in the same expression"
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
//#endregion
|
|
316
|
+
//#region src/plugins/core/queries/gettext.ts
|
|
317
|
+
const gettextQuery = withComment({
|
|
318
|
+
pattern: callPattern("gettext", messageArgs),
|
|
319
|
+
extract: extractMessage("gettext")
|
|
320
|
+
});
|
|
321
|
+
const allowed = new Set([
|
|
322
|
+
"string",
|
|
323
|
+
"object",
|
|
324
|
+
"template_string",
|
|
325
|
+
"identifier",
|
|
326
|
+
"call_expression"
|
|
327
|
+
]);
|
|
328
|
+
const gettextInvalidQuery = {
|
|
329
|
+
pattern: callPattern("gettext", "(arguments (_) @arg)"),
|
|
330
|
+
extract(match) {
|
|
331
|
+
const call = match.captures.find((c) => c.name === "call")?.node;
|
|
332
|
+
const node = match.captures.find((c) => c.name === "arg")?.node;
|
|
333
|
+
if (!call || !node) return void 0;
|
|
334
|
+
if (allowed.has(node.type)) return void 0;
|
|
335
|
+
return {
|
|
336
|
+
node,
|
|
337
|
+
error: "gettext() argument must be a string literal, object literal, or template literal"
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
//#endregion
|
|
343
|
+
//#region src/plugins/core/queries/ngettext.ts
|
|
344
|
+
const msgCall$2 = callPattern("message", messageArgs).replace(/@call/g, "@msg").replace(/@func/g, "@msgfn");
|
|
345
|
+
const plainMsg$1 = `(${messageArg}) @msg`;
|
|
346
|
+
const msgArg$1 = `[${msgCall$2} ${plainMsg$1}]`;
|
|
347
|
+
const ngettextQuery = withComment({
|
|
348
|
+
pattern: callPattern("ngettext", `(arguments ${msgArg$1} "," ${msgArg$1} ("," ${msgArg$1})* "," (_) @n)`),
|
|
349
|
+
extract: extractPluralForms("ngettext")
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
//#endregion
|
|
353
|
+
//#region src/plugins/core/queries/npgettext.ts
|
|
354
|
+
const msgCall$1 = callPattern("message", messageArgs).replace(/@call/g, "@msg").replace(/@func/g, "@msgfn");
|
|
355
|
+
const plainMsg = `(${messageArg}) @msg`;
|
|
356
|
+
const msgArg = `[${msgCall$1} ${plainMsg}]`;
|
|
357
|
+
const npgettextQuery = withComment({
|
|
358
|
+
pattern: callPattern("npgettext", `(arguments (string (string_fragment) @msgctxt) "," ${msgArg} "," ${msgArg} ("," ${msgArg})* "," (_) @n)`),
|
|
359
|
+
extract: extractPluralForms("npgettext")
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
//#endregion
|
|
363
|
+
//#region src/plugins/core/queries/pgettext.ts
|
|
364
|
+
const pgettextQuery = withComment({
|
|
365
|
+
pattern: callPattern("pgettext", `(arguments (string (string_fragment) @msgctxt) "," ${messageArg})`),
|
|
366
|
+
extract(match) {
|
|
367
|
+
const result = extractMessage("pgettext")(match);
|
|
368
|
+
const contextNode = match.captures.find((c) => c.name === "msgctxt")?.node;
|
|
369
|
+
if (!result || !contextNode || !result.translation) return result;
|
|
370
|
+
return {
|
|
371
|
+
node: result.node,
|
|
372
|
+
translation: {
|
|
373
|
+
...result.translation,
|
|
374
|
+
context: contextNode.text
|
|
375
|
+
}
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
//#endregion
|
|
381
|
+
//#region src/plugins/core/queries/plural.ts
|
|
382
|
+
const msgCall = callPattern("message", messageArgs, false).replace(/@call/g, "@msg").replace(/@func/g, "@msgfn");
|
|
383
|
+
const pluralQuery = withComment({
|
|
384
|
+
pattern: callPattern("plural", `(arguments (
|
|
385
|
+
(${msgCall} ("," )?)+
|
|
386
|
+
(number) @n
|
|
387
|
+
))`, false),
|
|
388
|
+
extract: extractPluralForms("plural")
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
//#endregion
|
|
392
|
+
//#region src/plugins/core/queries/index.ts
|
|
393
|
+
const queries = [
|
|
394
|
+
messageQuery,
|
|
395
|
+
messageInvalidQuery,
|
|
396
|
+
gettextQuery,
|
|
397
|
+
gettextInvalidQuery,
|
|
398
|
+
pluralQuery,
|
|
399
|
+
ngettextQuery,
|
|
400
|
+
pgettextQuery,
|
|
401
|
+
npgettextQuery,
|
|
402
|
+
contextMsgQuery,
|
|
403
|
+
contextPluralQuery,
|
|
404
|
+
contextInvalidQuery
|
|
405
|
+
];
|
|
406
|
+
|
|
407
|
+
//#endregion
|
|
408
|
+
//#region src/plugins/core/parse.ts
|
|
409
|
+
function getLanguage(ext) {
|
|
410
|
+
switch (ext) {
|
|
411
|
+
case ".ts": return TS.typescript;
|
|
412
|
+
case ".tsx": return TS.tsx;
|
|
413
|
+
default: return JavaScript;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
const getCachedParser = memo(function getCachedParser$1(ext) {
|
|
417
|
+
const parser = new Parser();
|
|
418
|
+
const language = getLanguage(ext);
|
|
419
|
+
parser.setLanguage(language);
|
|
420
|
+
return {
|
|
421
|
+
parser,
|
|
422
|
+
language
|
|
423
|
+
};
|
|
424
|
+
});
|
|
425
|
+
function getParser(path$1) {
|
|
426
|
+
const ext = extname(path$1);
|
|
427
|
+
return getCachedParser(ext);
|
|
428
|
+
}
|
|
429
|
+
function parseSource(source, path$1) {
|
|
430
|
+
const context = { path: path$1 };
|
|
431
|
+
const { parser, language } = getParser(path$1);
|
|
432
|
+
const tree = parser.parse(source);
|
|
433
|
+
const translations = [];
|
|
434
|
+
const imports = [];
|
|
435
|
+
const seen = /* @__PURE__ */ new Set();
|
|
436
|
+
for (const spec of queries) {
|
|
437
|
+
const query = new Parser.Query(language, spec.pattern);
|
|
438
|
+
for (const match of query.matches(tree.rootNode)) {
|
|
439
|
+
const message = spec.extract(match);
|
|
440
|
+
if (!message) continue;
|
|
441
|
+
const { node, translation, error } = message;
|
|
442
|
+
if (seen.has(node.id)) continue;
|
|
443
|
+
seen.add(node.id);
|
|
444
|
+
const reference = getReference(node, context);
|
|
445
|
+
if (translation) translations.push({
|
|
446
|
+
...translation,
|
|
447
|
+
comments: {
|
|
448
|
+
...translation.comments,
|
|
449
|
+
reference
|
|
450
|
+
}
|
|
451
|
+
});
|
|
452
|
+
if (error) console.warn(`Parsing error at ${reference}: ${error}`);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
const importTreeQuery = new Parser.Query(language, importQuery.pattern);
|
|
456
|
+
for (const match of importTreeQuery.matches(tree.rootNode)) {
|
|
457
|
+
const imp = importQuery.extract(match);
|
|
458
|
+
if (imp) imports.push(imp);
|
|
459
|
+
}
|
|
460
|
+
return {
|
|
461
|
+
translations,
|
|
462
|
+
imports
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
//#endregion
|
|
467
|
+
//#region src/plugins/core/resolve.ts
|
|
468
|
+
function findTsconfig(dir) {
|
|
469
|
+
let current = dir;
|
|
470
|
+
while (true) {
|
|
471
|
+
const config = path.join(current, "tsconfig.json");
|
|
472
|
+
if (fs.existsSync(config)) return config;
|
|
473
|
+
const parent = path.dirname(current);
|
|
474
|
+
if (parent === current) return void 0;
|
|
475
|
+
current = parent;
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
const resolverCache = /* @__PURE__ */ new Map();
|
|
479
|
+
function getResolver(dir) {
|
|
480
|
+
const tsconfig = findTsconfig(dir);
|
|
481
|
+
const key = tsconfig ?? "__default__";
|
|
482
|
+
let resolver = resolverCache.get(key);
|
|
483
|
+
if (!resolver) {
|
|
484
|
+
resolver = new ResolverFactory({
|
|
485
|
+
extensions: [
|
|
486
|
+
".ts",
|
|
487
|
+
".tsx",
|
|
488
|
+
".js",
|
|
489
|
+
".jsx",
|
|
490
|
+
".mjs",
|
|
491
|
+
".cjs",
|
|
492
|
+
".json"
|
|
493
|
+
],
|
|
494
|
+
conditionNames: [
|
|
495
|
+
"import",
|
|
496
|
+
"require",
|
|
497
|
+
"node"
|
|
498
|
+
],
|
|
499
|
+
...tsconfig ? { tsconfig: { configFile: tsconfig } } : {}
|
|
500
|
+
});
|
|
501
|
+
resolverCache.set(key, resolver);
|
|
502
|
+
}
|
|
503
|
+
return resolver;
|
|
504
|
+
}
|
|
505
|
+
function resolveImports(file, imports) {
|
|
506
|
+
const dir = path.dirname(path.resolve(file));
|
|
507
|
+
const resolver = getResolver(dir);
|
|
508
|
+
const resolved = [];
|
|
509
|
+
for (const spec of imports) {
|
|
510
|
+
const res = resolver.sync(dir, spec);
|
|
511
|
+
if (res.path) resolved.push(res.path);
|
|
512
|
+
}
|
|
513
|
+
return resolved;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
//#endregion
|
|
517
|
+
//#region src/plugins/core/core.ts
|
|
518
|
+
const filter = /\.([cm]?tsx?|jsx?)$/;
|
|
519
|
+
function core() {
|
|
520
|
+
return {
|
|
521
|
+
name: "core",
|
|
522
|
+
setup(build) {
|
|
523
|
+
build.onResolve({ filter: /.*/ }, ({ entrypoint, path: path$1 }) => {
|
|
524
|
+
return {
|
|
525
|
+
entrypoint,
|
|
526
|
+
path: resolve(path$1)
|
|
527
|
+
};
|
|
528
|
+
});
|
|
529
|
+
build.onLoad({ filter }, async ({ entrypoint, path: path$1 }) => {
|
|
530
|
+
const contents = await readFile(path$1, "utf8");
|
|
531
|
+
return {
|
|
532
|
+
entrypoint,
|
|
533
|
+
path: path$1,
|
|
534
|
+
contents
|
|
535
|
+
};
|
|
536
|
+
});
|
|
537
|
+
build.onExtract({ filter }, ({ entrypoint, path: path$1, contents }) => {
|
|
538
|
+
const { translations, imports } = parseSource(contents, path$1);
|
|
539
|
+
if (build.context.config.walk) {
|
|
540
|
+
const paths = resolveImports(path$1, imports);
|
|
541
|
+
for (const path$2 of paths) build.resolvePath({
|
|
542
|
+
entrypoint,
|
|
543
|
+
path: path$2
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
return {
|
|
547
|
+
entrypoint,
|
|
548
|
+
path: path$1,
|
|
549
|
+
translations
|
|
550
|
+
};
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
//#endregion
|
|
557
|
+
//#region src/plugins/po/po.ts
|
|
558
|
+
function formatDate(date) {
|
|
559
|
+
const pad = (n) => n.toString().padStart(2, "0");
|
|
560
|
+
const year = date.getFullYear();
|
|
561
|
+
const month = pad(date.getMonth() + 1);
|
|
562
|
+
const day = pad(date.getDate());
|
|
563
|
+
const hours = pad(date.getHours());
|
|
564
|
+
const minutes = pad(date.getMinutes());
|
|
565
|
+
const tzo = -date.getTimezoneOffset();
|
|
566
|
+
const sign = tzo >= 0 ? "+" : "-";
|
|
567
|
+
const offsetHours = pad(Math.floor(Math.abs(tzo) / 60));
|
|
568
|
+
const offsetMinutes = pad(Math.abs(tzo) % 60);
|
|
569
|
+
return `${year}-${month}-${day} ${hours}:${minutes}${sign}${offsetHours}${offsetMinutes}`;
|
|
570
|
+
}
|
|
571
|
+
function collect(source, locale) {
|
|
572
|
+
const translations = { "": {} };
|
|
573
|
+
const nplurals = locale ? getNPlurals(locale) : void 0;
|
|
574
|
+
for (const { context, id, message, comments, obsolete, plural } of source) {
|
|
575
|
+
const ctx = context || "";
|
|
576
|
+
if (!translations[ctx]) translations[ctx] = {};
|
|
577
|
+
const length = plural ? nplurals ?? message.length : 1;
|
|
578
|
+
translations[ctx][id] = {
|
|
579
|
+
msgctxt: context || void 0,
|
|
580
|
+
msgid: id,
|
|
581
|
+
msgid_plural: plural,
|
|
582
|
+
msgstr: Array.from({ length }, () => ""),
|
|
583
|
+
comments,
|
|
584
|
+
obsolete
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
return translations;
|
|
588
|
+
}
|
|
589
|
+
function merge(locale, sources, existing, strategy = "mark", timestamp = /* @__PURE__ */ new Date()) {
|
|
590
|
+
let headers = {};
|
|
591
|
+
let translations = { "": {} };
|
|
592
|
+
const nplurals = getNPlurals(locale);
|
|
593
|
+
if (existing) {
|
|
594
|
+
const parsed = gettextParser.po.parse(existing);
|
|
595
|
+
headers = parsed.headers || {};
|
|
596
|
+
translations = parsed.translations || { "": {} };
|
|
597
|
+
for (const ctx of Object.keys(translations)) for (const id of Object.keys(translations[ctx])) {
|
|
598
|
+
if (ctx === "" && id === "") continue;
|
|
599
|
+
translations[ctx][id].obsolete = true;
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
const collected = sources.reduce((acc, { translations: translations$1 }) => assign(acc, translations$1), { "": {} });
|
|
603
|
+
for (const [ctx, msgs] of Object.entries(collected)) {
|
|
604
|
+
if (!translations[ctx]) translations[ctx] = {};
|
|
605
|
+
for (const [id, entry] of Object.entries(msgs)) {
|
|
606
|
+
const existingEntry = translations[ctx][id];
|
|
607
|
+
if (existingEntry) {
|
|
608
|
+
entry.msgstr = existingEntry.msgstr;
|
|
609
|
+
entry.comments = {
|
|
610
|
+
...entry.comments,
|
|
611
|
+
translator: existingEntry.comments?.translator
|
|
612
|
+
};
|
|
613
|
+
}
|
|
614
|
+
entry.obsolete = false;
|
|
615
|
+
entry.msgstr = entry.msgstr.slice(0, nplurals);
|
|
616
|
+
while (entry.msgstr.length < nplurals) entry.msgstr.push("");
|
|
617
|
+
translations[ctx][id] = entry;
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
headers = {
|
|
621
|
+
...headers,
|
|
622
|
+
"content-type": headers["content-type"] || "text/plain; charset=UTF-8",
|
|
623
|
+
"plural-forms": `nplurals=${nplurals}; plural=${getFormula(locale)};`,
|
|
624
|
+
language: locale,
|
|
625
|
+
"pot-creation-date": formatDate(timestamp),
|
|
626
|
+
"x-generator": "@let-value/translate-extract"
|
|
627
|
+
};
|
|
628
|
+
if (strategy === "remove") {
|
|
629
|
+
for (const ctx of Object.keys(translations)) for (const id of Object.keys(translations[ctx])) if (translations[ctx][id].obsolete) delete translations[ctx][id];
|
|
630
|
+
}
|
|
631
|
+
const poObj = {
|
|
632
|
+
charset: "utf-8",
|
|
633
|
+
headers,
|
|
634
|
+
translations
|
|
635
|
+
};
|
|
636
|
+
return gettextParser.po.compile(poObj).toString();
|
|
637
|
+
}
|
|
638
|
+
function po() {
|
|
639
|
+
return {
|
|
640
|
+
name: "po",
|
|
641
|
+
setup(build) {
|
|
642
|
+
build.onCollect({ filter: /.*/ }, ({ entrypoint, translations,...rest }, ctx) => {
|
|
643
|
+
const record = collect(translations, ctx.locale);
|
|
644
|
+
const destination = `${basename(entrypoint, extname(entrypoint))}.po`;
|
|
645
|
+
return {
|
|
646
|
+
...rest,
|
|
647
|
+
entrypoint,
|
|
648
|
+
destination,
|
|
649
|
+
translations: record
|
|
650
|
+
};
|
|
651
|
+
});
|
|
652
|
+
build.onGenerate({ filter: /.*\/po$/ }, async ({ path: path$1, locale, collected }, ctx) => {
|
|
653
|
+
const existing = await fs$1.readFile(path$1).catch(() => void 0);
|
|
654
|
+
const out = merge(locale, collected, existing, ctx.config.obsolete, ctx.generatedAt);
|
|
655
|
+
await fs$1.mkdir(dirname(path$1), { recursive: true });
|
|
656
|
+
await fs$1.writeFile(path$1, out);
|
|
657
|
+
});
|
|
658
|
+
}
|
|
659
|
+
};
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
//#endregion
|
|
663
|
+
//#region src/configuration.ts
|
|
664
|
+
const defaultPlugins = [core(), po()];
|
|
665
|
+
const defaultDestination = (locale, _entrypoint, path$1) => join(locale, path$1);
|
|
666
|
+
function defineConfig(config) {
|
|
667
|
+
let plugins;
|
|
668
|
+
const user = config.plugins;
|
|
669
|
+
if (typeof user === "function") plugins = user(defaultPlugins);
|
|
670
|
+
else if (Array.isArray(user)) plugins = [...defaultPlugins, ...user];
|
|
671
|
+
else plugins = defaultPlugins;
|
|
672
|
+
const raw = Array.isArray(config.entrypoints) ? config.entrypoints : [config.entrypoints];
|
|
673
|
+
const entrypoints = [];
|
|
674
|
+
for (const ep of raw) if (typeof ep === "string") {
|
|
675
|
+
const paths = globSync(ep, { nodir: true });
|
|
676
|
+
if (paths.length === 0) entrypoints.push({ entrypoint: ep });
|
|
677
|
+
else for (const path$1 of paths) entrypoints.push({ entrypoint: path$1 });
|
|
678
|
+
} else {
|
|
679
|
+
const { entrypoint, destination: destination$1, obsolete: obsolete$1 } = ep;
|
|
680
|
+
const paths = globSync(entrypoint, { nodir: true });
|
|
681
|
+
if (paths.length === 0) entrypoints.push({
|
|
682
|
+
entrypoint,
|
|
683
|
+
destination: destination$1,
|
|
684
|
+
obsolete: obsolete$1
|
|
685
|
+
});
|
|
686
|
+
else for (const path$1 of paths) entrypoints.push({
|
|
687
|
+
entrypoint: path$1,
|
|
688
|
+
destination: destination$1,
|
|
689
|
+
obsolete: obsolete$1
|
|
690
|
+
});
|
|
691
|
+
}
|
|
692
|
+
const defaultLocale = config.defaultLocale ?? "en";
|
|
693
|
+
const locales = config.locales ?? [defaultLocale];
|
|
694
|
+
const destination = config.destination ?? defaultDestination;
|
|
695
|
+
const obsolete = config.obsolete ?? "mark";
|
|
696
|
+
const walk = config.walk ?? true;
|
|
697
|
+
return {
|
|
698
|
+
plugins,
|
|
699
|
+
entrypoints,
|
|
700
|
+
defaultLocale,
|
|
701
|
+
locales,
|
|
702
|
+
destination,
|
|
703
|
+
obsolete,
|
|
704
|
+
walk
|
|
705
|
+
};
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
//#endregion
|
|
709
|
+
export { defineConfig };
|
|
710
|
+
//# sourceMappingURL=index.js.map
|