@x18n/builtins 0.0.13 → 0.0.14
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/angular-templates-DqIOGfzT.mjs +8 -0
- package/dist/extractors.mjs +7 -3
- package/dist/finders.d.mts +4 -1
- package/dist/finders.mjs +1 -1
- package/dist/formats/ecmascript.d.mts +5 -0
- package/dist/formats/ecmascript.mjs +211 -0
- package/dist/formats/ftl.d.mts +5 -0
- package/dist/formats/ftl.mjs +234 -0
- package/dist/formats/ini.d.mts +5 -0
- package/dist/formats/ini.mjs +158 -0
- package/dist/formats/json.d.mts +6 -0
- package/dist/formats/json.mjs +10 -0
- package/dist/formats/json5.d.mts +5 -0
- package/dist/formats/json5.mjs +30 -0
- package/dist/formats/jsonc.d.mts +6 -0
- package/dist/formats/jsonc.mjs +10 -0
- package/dist/formats/php.d.mts +5 -0
- package/dist/formats/php.mjs +200 -0
- package/dist/formats/po.d.mts +5 -0
- package/dist/formats/po.mjs +177 -0
- package/dist/formats/properties.d.mts +5 -0
- package/dist/formats/properties.mjs +123 -0
- package/dist/formats/resx.d.mts +6 -0
- package/dist/formats/resx.mjs +202 -0
- package/dist/formats/yaml.d.mts +5 -0
- package/dist/formats/yaml.mjs +113 -0
- package/dist/formats.d.mts +10 -34
- package/dist/formats.mjs +61 -1
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +28 -22
- package/dist/{vue-CF4pe-Yr.mjs → js-CV-ECNJG.mjs} +67 -222
- package/dist/json-CyjI8bjp.d.mts +8 -0
- package/dist/json-Db-8qSbE.mjs +73 -0
- package/dist/jsx-templates-B1J0no3i.mjs +17 -0
- package/dist/locale-root-C_rch3IQ.mjs +15 -0
- package/dist/manifests.d.mts +8 -0
- package/dist/manifests.mjs +66 -2
- package/dist/placeholders.d.mts +11 -0
- package/dist/placeholders.mjs +28 -1
- package/dist/plugin-Cb1b0axD.mjs +83 -0
- package/dist/plural-forms-B7_QX4nG.mjs +12 -0
- package/dist/presets/cargo-i18n.d.mts +6 -0
- package/dist/presets/cargo-i18n.mjs +28 -0
- package/dist/presets/chrome-extension.d.mts +6 -0
- package/dist/presets/chrome-extension.mjs +52 -0
- package/dist/presets/dotnet-resx.d.mts +6 -0
- package/dist/presets/dotnet-resx.mjs +64 -0
- package/dist/presets/easy-localization.d.mts +6 -0
- package/dist/presets/easy-localization.mjs +36 -0
- package/dist/presets/ember-intl.d.mts +5 -0
- package/dist/presets/ember-intl.mjs +31 -0
- package/dist/presets/fluent-react.d.mts +5 -0
- package/dist/presets/fluent-react.mjs +46 -0
- package/dist/presets/fluent-vue.d.mts +5 -0
- package/dist/presets/fluent-vue.mjs +51 -0
- package/dist/presets/flutter-translate.d.mts +6 -0
- package/dist/presets/flutter-translate.mjs +30 -0
- package/dist/presets/flutter.d.mts +6 -0
- package/dist/presets/flutter.mjs +21 -0
- package/dist/presets/general.d.mts +6 -0
- package/dist/presets/general.mjs +17 -0
- package/dist/presets/globalize.d.mts +5 -0
- package/dist/presets/globalize.mjs +25 -0
- package/dist/presets/gravity-ui-i18n.d.mts +6 -0
- package/dist/presets/gravity-ui-i18n.mjs +41 -0
- package/dist/presets/i18n-js.d.mts +5 -0
- package/dist/presets/i18n-js.mjs +19 -0
- package/dist/presets/i18n-tag.d.mts +5 -0
- package/dist/presets/i18n-tag.mjs +21 -0
- package/dist/presets/i18next-shopify.d.mts +6 -0
- package/dist/presets/i18next-shopify.mjs +23 -0
- package/dist/presets/i18next.d.mts +5 -0
- package/dist/presets/i18next.mjs +10 -0
- package/dist/presets/i18nice.d.mts +6 -0
- package/dist/presets/i18nice.mjs +29 -0
- package/dist/presets/jekyll.d.mts +5 -0
- package/dist/presets/jekyll.mjs +23 -0
- package/dist/presets/laravel-vue-i18n.d.mts +6 -0
- package/dist/presets/laravel-vue-i18n.mjs +54 -0
- package/dist/presets/laravel.d.mts +6 -0
- package/dist/presets/laravel.mjs +23 -0
- package/dist/presets/lingui.d.mts +5 -0
- package/dist/presets/lingui.mjs +61 -0
- package/dist/presets/next-international.d.mts +5 -0
- package/dist/presets/next-international.mjs +27 -0
- package/dist/presets/next-intl.d.mts +5 -0
- package/dist/presets/next-intl.mjs +55 -0
- package/dist/presets/next-translate.d.mts +5 -0
- package/dist/presets/next-translate.mjs +53 -0
- package/dist/presets/ngx-translate.d.mts +5 -0
- package/dist/presets/ngx-translate.mjs +41 -0
- package/dist/presets/nuxt-i18n-micro.d.mts +5 -0
- package/dist/presets/nuxt-i18n-micro.mjs +81 -0
- package/dist/presets/nuxt-i18n.d.mts +5 -0
- package/dist/presets/nuxt-i18n.mjs +23 -0
- package/dist/presets/paraglide.d.mts +5 -0
- package/dist/presets/paraglide.mjs +38 -0
- package/dist/presets/php-gettext.d.mts +6 -0
- package/dist/presets/php-gettext.mjs +27 -0
- package/dist/presets/php-joomla.d.mts +6 -0
- package/dist/presets/php-joomla.mjs +22 -0
- package/dist/presets/polyglot.d.mts +5 -0
- package/dist/presets/polyglot.mjs +23 -0
- package/dist/presets/python-gettext.d.mts +6 -0
- package/dist/presets/python-gettext.mjs +44 -0
- package/dist/presets/react-i18next.d.mts +5 -0
- package/dist/presets/react-i18next.mjs +67 -0
- package/dist/presets/react-intl.d.mts +5 -0
- package/dist/presets/react-intl.mjs +47 -0
- package/dist/presets/ruby-rails.d.mts +7 -0
- package/dist/presets/ruby-rails.mjs +44 -0
- package/dist/presets/solid-primitives-i18n.d.mts +5 -0
- package/dist/presets/solid-primitives-i18n.mjs +29 -0
- package/dist/presets/svelte-i18n.d.mts +5 -0
- package/dist/presets/svelte-i18n.mjs +33 -0
- package/dist/presets/svelte-i18next.d.mts +5 -0
- package/dist/presets/svelte-i18next.mjs +27 -0
- package/dist/presets/transloco.d.mts +5 -0
- package/dist/presets/transloco.mjs +44 -0
- package/dist/presets/typesafe-i18n.d.mts +6 -0
- package/dist/presets/typesafe-i18n.mjs +61 -0
- package/dist/presets/ui5.d.mts +5 -0
- package/dist/presets/ui5.mjs +35 -0
- package/dist/presets/vscode-extension.d.mts +5 -0
- package/dist/presets/vscode-extension.mjs +53 -0
- package/dist/presets/vue-i18n.d.mts +5 -0
- package/dist/presets/vue-i18n.mjs +59 -0
- package/dist/presets.d.mts +7 -54
- package/dist/presets.mjs +346 -1
- package/dist/reporters/github.d.mts +6 -0
- package/dist/reporters/github.mjs +27 -0
- package/dist/reporters/json.d.mts +6 -0
- package/dist/reporters/json.mjs +24 -0
- package/dist/reporters/markdown.d.mts +6 -0
- package/dist/reporters/markdown.mjs +37 -0
- package/dist/reporters/pretty.d.mts +6 -0
- package/dist/reporters/pretty.mjs +64 -0
- package/dist/reporters/sarif.d.mts +6 -0
- package/dist/reporters/sarif.mjs +48 -0
- package/dist/reporters.d.mts +5 -12
- package/dist/reporters.mjs +8 -218
- package/dist/scanners/astro.d.mts +6 -0
- package/dist/scanners/astro.mjs +256 -0
- package/dist/scanners/js.d.mts +6 -0
- package/dist/scanners/js.mjs +33 -0
- package/dist/scanners/regex-scopes.d.mts +14 -0
- package/dist/scanners/regex-scopes.mjs +41 -0
- package/dist/scanners/regex.d.mts +9 -0
- package/dist/{regex-AYVhuSb6.mjs → scanners/regex.mjs} +2 -40
- package/dist/scanners/svelte.d.mts +6 -0
- package/dist/{scanners-DhnnVp7s.mjs → scanners/svelte.mjs} +3 -3
- package/dist/scanners/vue.d.mts +6 -0
- package/dist/scanners/vue.mjs +172 -0
- package/dist/scanners.d.mts +9 -50
- package/dist/scanners.mjs +36 -4
- package/dist/sfc-blocks-DaGKDZsb.mjs +213 -0
- package/dist/shared-BWaXUmwA.mjs +70 -0
- package/dist/shared-D-tbsYmc.mjs +30 -0
- package/dist/shared-D4NJBKEP.mjs +17 -0
- package/dist/shared-Tq06G0gp.mjs +6 -0
- package/dist/shared-meYeVckh.mjs +11 -0
- package/dist/vue-compiler-0sfLIiiL.mjs +8 -0
- package/package.json +67 -3
- package/dist/formats-CFkM5co0.mjs +0 -1370
- package/dist/presets-Co5SlnKr.mjs +0 -1187
- package/dist/sfc-blocks-BqQ8m9rX.mjs +0 -161
- package/dist/yaml-plugin.d.mts +0 -8
- package/dist/yaml-plugin.mjs +0 -68
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
//#region src/scanners/js.ts
|
|
1
|
+
//#region src/scanners/shared/js.ts
|
|
2
2
|
const textAttributeWords = /* @__PURE__ */ new Set([
|
|
3
3
|
"title",
|
|
4
4
|
"subtitle",
|
|
@@ -83,6 +83,8 @@ var FileScan = class {
|
|
|
83
83
|
declared = /* @__PURE__ */ new Map();
|
|
84
84
|
factories = /* @__PURE__ */ new Map();
|
|
85
85
|
typed = /* @__PURE__ */ new Map();
|
|
86
|
+
/** Members of the file's top-level interfaces and object type aliases, for typed props (`({ t }: Props)`). */
|
|
87
|
+
typeMembers = /* @__PURE__ */ new Map();
|
|
86
88
|
consumed = /* @__PURE__ */ new Set();
|
|
87
89
|
/** Render-prop functions of factory components (`<Translation>{t => ...}</Translation>`), by start offset. */
|
|
88
90
|
renderProps = /* @__PURE__ */ new Map();
|
|
@@ -145,6 +147,11 @@ var FileScan = class {
|
|
|
145
147
|
return constant && isStringLiteral(constant) ? constant.value : void 0;
|
|
146
148
|
}
|
|
147
149
|
}
|
|
150
|
+
/** A fixed namespace, or the one an argument holds. */
|
|
151
|
+
namespaceArg(args, source, fallback) {
|
|
152
|
+
if (typeof source === "string") return source;
|
|
153
|
+
return source ? this.stringArg(this.argValues(args, source, fallback)[0]) : void 0;
|
|
154
|
+
}
|
|
148
155
|
argValues(args, source, fallback) {
|
|
149
156
|
const arg = args[typeof source === "number" ? source : source?.arg ?? fallback];
|
|
150
157
|
const property = typeof source === "object" ? source.property : void 0;
|
|
@@ -165,13 +172,9 @@ var FileScan = class {
|
|
|
165
172
|
}
|
|
166
173
|
/** ---- Usages ---- */
|
|
167
174
|
/** `expression` is the call holding `value`, kept only when the key is the call's own literal. */
|
|
168
|
-
addKey(value,
|
|
175
|
+
addKey(value, base, expression) {
|
|
169
176
|
if ("props" in value) return;
|
|
170
177
|
const node = unwrap(value);
|
|
171
|
-
const base = {
|
|
172
|
-
namespace: namespace ?? binding.namespace,
|
|
173
|
-
keyPrefix: binding.keyPrefix
|
|
174
|
-
};
|
|
175
178
|
const call = expression ? { expression: this.range(expression) } : {};
|
|
176
179
|
if (isStringLiteral(node)) {
|
|
177
180
|
this.consumed.add(node.start);
|
|
@@ -187,7 +190,7 @@ var FileScan = class {
|
|
|
187
190
|
return;
|
|
188
191
|
}
|
|
189
192
|
if (node.type === "JSXExpressionContainer") {
|
|
190
|
-
if (node.expression.type !== "JSXEmptyExpression") this.addKey(node.expression,
|
|
193
|
+
if (node.expression.type !== "JSXEmptyExpression") this.addKey(node.expression, base);
|
|
191
194
|
return;
|
|
192
195
|
}
|
|
193
196
|
if (node.type === "TemplateLiteral") {
|
|
@@ -209,8 +212,8 @@ var FileScan = class {
|
|
|
209
212
|
return;
|
|
210
213
|
}
|
|
211
214
|
if (node.type === "ConditionalExpression") {
|
|
212
|
-
this.addKey(node.consequent,
|
|
213
|
-
this.addKey(node.alternate,
|
|
215
|
+
this.addKey(node.consequent, base);
|
|
216
|
+
this.addKey(node.alternate, base);
|
|
214
217
|
return;
|
|
215
218
|
}
|
|
216
219
|
if (node.type === "ArrowFunctionExpression" && node.params[0]?.type === "Identifier") return this.addSelector(node.params[0].name, node.body, base);
|
|
@@ -225,6 +228,10 @@ var FileScan = class {
|
|
|
225
228
|
});
|
|
226
229
|
return;
|
|
227
230
|
}
|
|
231
|
+
this.addUnknown(node, base);
|
|
232
|
+
}
|
|
233
|
+
addUnknown(node, base) {
|
|
234
|
+
if (isStringLiteral(node)) this.consumed.add(node.start);
|
|
228
235
|
this.usages.push({
|
|
229
236
|
...base,
|
|
230
237
|
raw: this.text.slice(node.start, node.end),
|
|
@@ -267,10 +274,17 @@ var FileScan = class {
|
|
|
267
274
|
handleCall(name, args, expression) {
|
|
268
275
|
const bindings = this.lookupAll(name).filter((binding) => !binding.spec.factory);
|
|
269
276
|
for (const binding of bindings) {
|
|
270
|
-
const namespace =
|
|
277
|
+
const namespace = this.namespaceArg(args, binding.spec.namespace, 1);
|
|
278
|
+
const keyPrefix = binding.spec.keyPrefix ? this.stringArg(this.argValues(args, binding.spec.keyPrefix, 1)[0]) : void 0;
|
|
279
|
+
const base = {
|
|
280
|
+
namespace: namespace ?? binding.namespace,
|
|
281
|
+
keyPrefix: keyPrefix ?? binding.keyPrefix
|
|
282
|
+
};
|
|
271
283
|
for (const value of this.argValues(args, binding.spec.key, 0)) {
|
|
272
|
-
if (
|
|
273
|
-
|
|
284
|
+
if ("props" in value) continue;
|
|
285
|
+
if (bindings.length > 1 && unwrap(value).type === "ObjectExpression") continue;
|
|
286
|
+
if (binding.spec.keyPrefix && keyPrefix === void 0) this.addUnknown(unwrap(value), base);
|
|
287
|
+
else this.addKey(value, base, expression);
|
|
274
288
|
}
|
|
275
289
|
}
|
|
276
290
|
return bindings.length > 0;
|
|
@@ -321,7 +335,7 @@ var FileScan = class {
|
|
|
321
335
|
/** The first factory spec whose namespace and prefix sources all resolve, else the first spec. */
|
|
322
336
|
pickFactory(args, specs) {
|
|
323
337
|
const resolved = specs.map((spec) => {
|
|
324
|
-
const namespace =
|
|
338
|
+
const namespace = this.namespaceArg(args, spec.namespace, 0);
|
|
325
339
|
const keyPrefix = spec.keyPrefix ? this.stringArg(this.argValues(args, spec.keyPrefix, 0)[0]) : void 0;
|
|
326
340
|
const complete = (!spec.namespace || namespace !== void 0) && (!spec.keyPrefix || keyPrefix !== void 0);
|
|
327
341
|
return {
|
|
@@ -354,17 +368,49 @@ var FileScan = class {
|
|
|
354
368
|
if (key && local.type === "Identifier" && this.declared.has(`${key}.t`)) this.bind(`${local.name}.t`, binding);
|
|
355
369
|
}
|
|
356
370
|
}
|
|
371
|
+
/** Parameters typed as a translate function (`t: TFunction<'ns'>`), also as destructured props. */
|
|
357
372
|
params(params) {
|
|
358
373
|
for (const param of params) {
|
|
359
374
|
const target = param.type === "TSParameterProperty" ? param.parameter : param;
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
375
|
+
const annotation = target.type === "Identifier" || target.type === "ObjectPattern" ? target.typeAnnotation?.typeAnnotation : void 0;
|
|
376
|
+
if (!annotation) continue;
|
|
377
|
+
if (target.type === "Identifier") this.bindTyped(target.name, annotation);
|
|
378
|
+
else if (target.type === "ObjectPattern") this.props(target, annotation);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
/** `({ t }: { t: TFunction<'ns'> })`, or with an interface or type of the file. */
|
|
382
|
+
props(pattern, annotation) {
|
|
383
|
+
const members = annotation.type === "TSTypeLiteral" ? annotation.members : annotation.type === "TSTypeReference" && annotation.typeName.type === "Identifier" ? this.typeMembers.get(annotation.typeName.name) ?? [] : [];
|
|
384
|
+
for (const property of pattern.properties) {
|
|
385
|
+
if (property.type !== "Property") continue;
|
|
386
|
+
const local = unwrap(property.value);
|
|
387
|
+
const name = propertyName(property.key);
|
|
388
|
+
const member = members.find((m) => m.type === "TSPropertySignature" && !m.computed && propertyName(m.key) === name);
|
|
389
|
+
const type = member?.type === "TSPropertySignature" ? member.typeAnnotation?.typeAnnotation : void 0;
|
|
390
|
+
if (local.type === "Identifier" && type) this.bindTyped(local.name, type);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
/** The namespace is the first type argument: `TFunction<'shared'>` or `TFunction<['shared', 'common']>`. */
|
|
394
|
+
bindTyped(name, annotation) {
|
|
395
|
+
if (annotation.type !== "TSTypeReference" || annotation.typeName.type !== "Identifier") return;
|
|
396
|
+
const spec = this.typed.get(annotation.typeName.name);
|
|
397
|
+
if (!spec) return;
|
|
398
|
+
const arg = annotation.typeArguments?.params[0];
|
|
399
|
+
const first = arg?.type === "TSTupleType" ? arg.elementTypes[0] : arg;
|
|
400
|
+
const namespace = first?.type === "TSLiteralType" && isStringLiteral(first.literal) ? first.literal.value : void 0;
|
|
401
|
+
this.bind(name, {
|
|
402
|
+
spec: {
|
|
365
403
|
...spec,
|
|
366
404
|
types: void 0
|
|
367
|
-
}
|
|
405
|
+
},
|
|
406
|
+
namespace
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
types(program) {
|
|
410
|
+
for (const statement of program.body) {
|
|
411
|
+
const node = statement.type === "ExportNamedDeclaration" && statement.declaration ? statement.declaration : statement;
|
|
412
|
+
if (node.type === "TSInterfaceDeclaration") this.typeMembers.set(node.id.name, node.body.body);
|
|
413
|
+
else if (node.type === "TSTypeAliasDeclaration" && node.typeAnnotation.type === "TSTypeLiteral") this.typeMembers.set(node.id.name, node.typeAnnotation.members);
|
|
368
414
|
}
|
|
369
415
|
}
|
|
370
416
|
importSpecifier(node) {
|
|
@@ -532,211 +578,10 @@ function scanSource(parse, text, options) {
|
|
|
532
578
|
range: false
|
|
533
579
|
});
|
|
534
580
|
const scan = new FileScan(source, (options.offset ?? 0) + shift, options.calls, options.hardStrings !== false, new Set(options.ignoreCallees), options.textAttributes);
|
|
581
|
+
scan.types(result.program);
|
|
535
582
|
scan.walk(result.program, void 0, "");
|
|
536
583
|
scan.markers(result.program, result.comments);
|
|
537
584
|
return scan.result();
|
|
538
585
|
}
|
|
539
|
-
/** The AST scanner for JS and TS, driven by call specs (`usage.calls`). */
|
|
540
|
-
function js() {
|
|
541
|
-
return {
|
|
542
|
-
name: "x18n:scanner-js",
|
|
543
|
-
scanners: [{
|
|
544
|
-
id: "js",
|
|
545
|
-
extensions: [
|
|
546
|
-
"js",
|
|
547
|
-
"jsx",
|
|
548
|
-
"mjs",
|
|
549
|
-
"cjs",
|
|
550
|
-
"ts",
|
|
551
|
-
"tsx",
|
|
552
|
-
"mts",
|
|
553
|
-
"cts"
|
|
554
|
-
],
|
|
555
|
-
async scan(file, ctx) {
|
|
556
|
-
return scanSource(await loadParser(), file.text, {
|
|
557
|
-
calls: ctx.calls,
|
|
558
|
-
filename: file.path,
|
|
559
|
-
hardStrings: true,
|
|
560
|
-
ignoreCallees: ctx.config.extract.ignoreCallees,
|
|
561
|
-
textAttributes: ctx.config.extract.attributes
|
|
562
|
-
});
|
|
563
|
-
}
|
|
564
|
-
}]
|
|
565
|
-
};
|
|
566
|
-
}
|
|
567
|
-
//#endregion
|
|
568
|
-
//#region src/scanners/vue.ts
|
|
569
|
-
let compiler;
|
|
570
|
-
function loadVueCompiler() {
|
|
571
|
-
compiler ??= import("@vue/compiler-sfc");
|
|
572
|
-
return compiler;
|
|
573
|
-
}
|
|
574
|
-
const ELEMENT = 1;
|
|
575
|
-
const TEXT = 2;
|
|
576
|
-
const SIMPLE_EXPRESSION = 4;
|
|
577
|
-
const INTERPOLATION = 5;
|
|
578
|
-
const ATTRIBUTE = 6;
|
|
579
|
-
/** `keypath` is the vue-i18n prop for a key, on `<i18n-t>` and on the app's own wrappers of it; `path` on the legacy components. */
|
|
580
|
-
function isKeyAttribute(tag, name) {
|
|
581
|
-
return name === "keypath" || name === "path" && (tag === "i18n" || tag === "i18n-d");
|
|
582
|
-
}
|
|
583
|
-
function merge(into, result) {
|
|
584
|
-
into.usages.push(...result.usages);
|
|
585
|
-
into.keySlots.push(...result.keySlots ?? []);
|
|
586
|
-
into.hardStrings.push(...result.hardStrings ?? []);
|
|
587
|
-
}
|
|
588
|
-
var TemplateScan = class {
|
|
589
|
-
parse;
|
|
590
|
-
calls;
|
|
591
|
-
textAttributes;
|
|
592
|
-
out = {
|
|
593
|
-
usages: [],
|
|
594
|
-
keySlots: [],
|
|
595
|
-
hardStrings: []
|
|
596
|
-
};
|
|
597
|
-
constructor(parse, calls, textAttributes) {
|
|
598
|
-
this.parse = parse;
|
|
599
|
-
this.calls = calls;
|
|
600
|
-
this.textAttributes = textAttributes;
|
|
601
|
-
}
|
|
602
|
-
expression(content, offset) {
|
|
603
|
-
if (!content.trim()) return;
|
|
604
|
-
try {
|
|
605
|
-
merge(this.out, scanSource(this.parse, content, {
|
|
606
|
-
calls: this.calls,
|
|
607
|
-
offset,
|
|
608
|
-
filename: "template.ts",
|
|
609
|
-
expression: true,
|
|
610
|
-
hardStrings: false
|
|
611
|
-
}));
|
|
612
|
-
} catch {}
|
|
613
|
-
}
|
|
614
|
-
/** `:keypath="cond ? 'a' : 'b'"`: the expression is read as the key argument of a translate call. */
|
|
615
|
-
keyExpression(content, offset) {
|
|
616
|
-
const callee = "__x18nKey(";
|
|
617
|
-
try {
|
|
618
|
-
const result = scanSource(this.parse, `${callee}${content})`, {
|
|
619
|
-
calls: [{ callee: "__x18nKey" }],
|
|
620
|
-
offset: offset - 10,
|
|
621
|
-
filename: "template.ts",
|
|
622
|
-
hardStrings: false
|
|
623
|
-
});
|
|
624
|
-
merge(this.out, {
|
|
625
|
-
...result,
|
|
626
|
-
usages: result.usages.map(({ expression: _, ...usage }) => usage)
|
|
627
|
-
});
|
|
628
|
-
} catch {}
|
|
629
|
-
}
|
|
630
|
-
/** `v-t="'key'"` and `v-t="{ path: 'key' }"`. */
|
|
631
|
-
vt(dir) {
|
|
632
|
-
const exp = dir.exp;
|
|
633
|
-
if (!exp || !("content" in exp)) return;
|
|
634
|
-
const offset = exp.loc.start.offset;
|
|
635
|
-
const literal = /^\s*(['"`])(.*)\1\s*$/.exec(exp.content) ?? /path\s*:\s*(['"`])(.*?)\1/.exec(exp.content);
|
|
636
|
-
if (!literal) return;
|
|
637
|
-
const start = offset + exp.content.indexOf(literal[2], literal.index);
|
|
638
|
-
const range = {
|
|
639
|
-
start,
|
|
640
|
-
end: start + literal[2].length
|
|
641
|
-
};
|
|
642
|
-
this.out.usages.push({
|
|
643
|
-
raw: literal[2],
|
|
644
|
-
range,
|
|
645
|
-
kind: "call",
|
|
646
|
-
dynamic: false
|
|
647
|
-
});
|
|
648
|
-
this.out.keySlots.push(range);
|
|
649
|
-
}
|
|
650
|
-
attribute(element, prop) {
|
|
651
|
-
if (prop.type === ATTRIBUTE) {
|
|
652
|
-
const value = prop.value;
|
|
653
|
-
if (!value) return;
|
|
654
|
-
const range = {
|
|
655
|
-
start: value.loc.start.offset + 1,
|
|
656
|
-
end: value.loc.end.offset - 1
|
|
657
|
-
};
|
|
658
|
-
if (isKeyAttribute(element.tag, prop.name)) {
|
|
659
|
-
this.out.usages.push({
|
|
660
|
-
raw: value.content,
|
|
661
|
-
range,
|
|
662
|
-
kind: "call",
|
|
663
|
-
dynamic: false
|
|
664
|
-
});
|
|
665
|
-
this.out.keySlots.push(range);
|
|
666
|
-
} else if (isTextAttribute(prop.name, this.textAttributes) && /\p{L}/u.test(value.content)) this.out.hardStrings.push({
|
|
667
|
-
text: value.content,
|
|
668
|
-
range: {
|
|
669
|
-
start: prop.loc.start.offset,
|
|
670
|
-
end: prop.loc.end.offset
|
|
671
|
-
},
|
|
672
|
-
kind: "vue-attribute",
|
|
673
|
-
attribute: prop.name
|
|
674
|
-
});
|
|
675
|
-
return;
|
|
676
|
-
}
|
|
677
|
-
if (prop.name === "t") {
|
|
678
|
-
this.vt(prop);
|
|
679
|
-
return;
|
|
680
|
-
}
|
|
681
|
-
const exp = prop.exp;
|
|
682
|
-
if (exp && "content" in exp) {
|
|
683
|
-
if (prop.name === "bind" && prop.arg && "content" in prop.arg && isKeyAttribute(element.tag, prop.arg.content)) this.keyExpression(exp.content, exp.loc.start.offset);
|
|
684
|
-
else this.expression(exp.content, exp.loc.start.offset);
|
|
685
|
-
}
|
|
686
|
-
}
|
|
687
|
-
node(node) {
|
|
688
|
-
if (node.type === INTERPOLATION && "content" in node.content) this.expression(node.content.content, node.content.loc.start.offset);
|
|
689
|
-
if (node.type === TEXT) {
|
|
690
|
-
const text = node.content.trim();
|
|
691
|
-
if (text && /\p{L}/u.test(text)) {
|
|
692
|
-
const start = node.loc.start.offset + node.content.indexOf(text);
|
|
693
|
-
this.out.hardStrings.push({
|
|
694
|
-
text,
|
|
695
|
-
range: {
|
|
696
|
-
start,
|
|
697
|
-
end: start + text.length
|
|
698
|
-
},
|
|
699
|
-
kind: "vue-template-text"
|
|
700
|
-
});
|
|
701
|
-
}
|
|
702
|
-
}
|
|
703
|
-
if (node.type === ELEMENT) for (const prop of node.props) this.attribute(node, prop);
|
|
704
|
-
if ("children" in node) {
|
|
705
|
-
for (const child of node.children) if (typeof child === "object" && child.type !== SIMPLE_EXPRESSION) this.node(child);
|
|
706
|
-
}
|
|
707
|
-
}
|
|
708
|
-
};
|
|
709
|
-
/** Vue SFCs: `<script>` through the `js` scanner, template expressions, `v-t`, `<i18n-t keypath>`. */
|
|
710
|
-
function vue() {
|
|
711
|
-
return {
|
|
712
|
-
name: "x18n:scanner-vue",
|
|
713
|
-
scanners: [{
|
|
714
|
-
id: "vue",
|
|
715
|
-
extensions: ["vue"],
|
|
716
|
-
async scan(file, ctx) {
|
|
717
|
-
const [{ parse }, parseJs] = await Promise.all([loadVueCompiler(), loadParser()]);
|
|
718
|
-
const { descriptor } = parse(file.text, {
|
|
719
|
-
filename: file.path,
|
|
720
|
-
sourceMap: false,
|
|
721
|
-
ignoreEmpty: false
|
|
722
|
-
});
|
|
723
|
-
const template = new TemplateScan(parseJs, ctx.calls, ctx.config.extract.attributes);
|
|
724
|
-
for (const script of [descriptor.script, descriptor.scriptSetup]) {
|
|
725
|
-
if (!script) continue;
|
|
726
|
-
const lang = script.lang ?? "js";
|
|
727
|
-
merge(template.out, scanSource(parseJs, script.content, {
|
|
728
|
-
calls: ctx.calls,
|
|
729
|
-
ignoreCallees: ctx.config.extract.ignoreCallees,
|
|
730
|
-
textAttributes: ctx.config.extract.attributes,
|
|
731
|
-
offset: script.loc.start.offset,
|
|
732
|
-
filename: `script.${lang === "tsx" || lang === "jsx" ? lang : lang === "ts" ? "ts" : "js"}`
|
|
733
|
-
}));
|
|
734
|
-
}
|
|
735
|
-
if (descriptor.template?.ast) template.node(descriptor.template.ast);
|
|
736
|
-
return template.out;
|
|
737
|
-
}
|
|
738
|
-
}]
|
|
739
|
-
};
|
|
740
|
-
}
|
|
741
586
|
//#endregion
|
|
742
|
-
export {
|
|
587
|
+
export { loadParser as n, scanSource as r, isTextAttribute as t };
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { n as detectEol, r as detectIndent } from "./shared-BWaXUmwA.mjs";
|
|
2
|
+
import { diffText, errors, pathId } from "@x18n/core";
|
|
3
|
+
import { applyEdits, modify, parseTree, printParseErrorCode } from "jsonc-parser";
|
|
4
|
+
//#region src/formats/shared/json.ts
|
|
5
|
+
function readTree(node, path, ranges) {
|
|
6
|
+
const out = {};
|
|
7
|
+
for (const property of node.children ?? []) {
|
|
8
|
+
const [keyNode, valueNode] = property.children ?? [];
|
|
9
|
+
if (!keyNode || !valueNode || typeof keyNode.value !== "string") continue;
|
|
10
|
+
const key = keyNode.value;
|
|
11
|
+
const childPath = [...path, key];
|
|
12
|
+
ranges.set(pathId(childPath), {
|
|
13
|
+
key: {
|
|
14
|
+
start: keyNode.offset + 1,
|
|
15
|
+
end: keyNode.offset + keyNode.length - 1
|
|
16
|
+
},
|
|
17
|
+
value: {
|
|
18
|
+
start: valueNode.offset,
|
|
19
|
+
end: valueNode.offset + valueNode.length
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
if (valueNode.type === "object") out[key] = readTree(valueNode, childPath, ranges);
|
|
23
|
+
else if (valueNode.type === "string" || valueNode.type === "number" || valueNode.type === "boolean" || valueNode.type === "null") out[key] = valueNode.value;
|
|
24
|
+
}
|
|
25
|
+
return out;
|
|
26
|
+
}
|
|
27
|
+
function createJsonFormat(id, extensions, allowComments, defaultIndent) {
|
|
28
|
+
return {
|
|
29
|
+
id,
|
|
30
|
+
extensions,
|
|
31
|
+
parse(text, ctx) {
|
|
32
|
+
const problems = [];
|
|
33
|
+
const root = parseTree(text, problems, {
|
|
34
|
+
allowTrailingComma: true,
|
|
35
|
+
disallowComments: !allowComments
|
|
36
|
+
});
|
|
37
|
+
if (problems.length > 0) {
|
|
38
|
+
const first = problems[0];
|
|
39
|
+
throw errors.X18N2002({
|
|
40
|
+
path: ctx.path,
|
|
41
|
+
reason: `${printParseErrorCode(first.error)} at offset ${first.offset}`
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
if (!root || root.type !== "object") {
|
|
45
|
+
if (text.trim() === "") return {
|
|
46
|
+
tree: {},
|
|
47
|
+
ranges: /* @__PURE__ */ new Map()
|
|
48
|
+
};
|
|
49
|
+
throw errors.X18N2003({ path: ctx.path });
|
|
50
|
+
}
|
|
51
|
+
const ranges = /* @__PURE__ */ new Map();
|
|
52
|
+
return {
|
|
53
|
+
tree: readTree(root, [], ranges),
|
|
54
|
+
ranges
|
|
55
|
+
};
|
|
56
|
+
},
|
|
57
|
+
edit(text, ops, ctx) {
|
|
58
|
+
const formattingOptions = {
|
|
59
|
+
...detectIndent(text, ctx.indent ?? defaultIndent),
|
|
60
|
+
eol: detectEol(text)
|
|
61
|
+
};
|
|
62
|
+
let next = text.trim() === "" ? "{}\n" : text;
|
|
63
|
+
for (const op of ops) next = applyEdits(next, modify(next, [...op.path], op.type === "set" ? op.value : void 0, { formattingOptions }));
|
|
64
|
+
return diffText(text, next);
|
|
65
|
+
},
|
|
66
|
+
serialize(tree, ctx) {
|
|
67
|
+
const indent = ctx.indent ?? defaultIndent ?? 2;
|
|
68
|
+
return `${JSON.stringify(tree, null, indent === "tab" ? " " : indent)}\n`;
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
//#endregion
|
|
73
|
+
export { createJsonFormat as t };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
//#region src/presets/shared/jsx-templates.ts
|
|
2
|
+
const jsxTemplates = [
|
|
3
|
+
{
|
|
4
|
+
template: "{t('{key}'{args})}",
|
|
5
|
+
kinds: ["jsx-text"]
|
|
6
|
+
},
|
|
7
|
+
{
|
|
8
|
+
template: "{attr}={t('{key}'{args})}",
|
|
9
|
+
kinds: ["jsx-attribute"]
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
template: "t('{key}'{args})",
|
|
13
|
+
kinds: ["js-string", "js-template"]
|
|
14
|
+
}
|
|
15
|
+
];
|
|
16
|
+
//#endregion
|
|
17
|
+
export { jsxTemplates as t };
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { isMessageLeaf } from "@x18n/core";
|
|
2
|
+
//#region src/presets/shared/locale-root.ts
|
|
3
|
+
/** Files that start with their locale, `en: { users: ... }`, as in Rails and i18nice. */
|
|
4
|
+
function localeRoot(applies) {
|
|
5
|
+
return {
|
|
6
|
+
applies,
|
|
7
|
+
read(tree, ctx) {
|
|
8
|
+
const root = tree[ctx.locale];
|
|
9
|
+
return root !== void 0 && !isMessageLeaf(root) ? root : {};
|
|
10
|
+
},
|
|
11
|
+
toStored: (path, ctx) => [ctx.locale, ...path]
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
//#endregion
|
|
15
|
+
export { localeRoot as t };
|
package/dist/manifests.d.mts
CHANGED
|
@@ -6,5 +6,13 @@ export declare const composerJson: ManifestReader;
|
|
|
6
6
|
export declare const pubspecYaml: ManifestReader;
|
|
7
7
|
/** Every `gem 'name'` line, not just `rails-i18n` as in i18n-ally. */
|
|
8
8
|
export declare const gemfile: ManifestReader;
|
|
9
|
+
/** Reads the PEP 621, PEP 735 and Poetry dependency tables; dependency names need no full TOML parser. */
|
|
10
|
+
export declare const pyprojectToml: ManifestReader;
|
|
11
|
+
export declare const requirementsTxt: ManifestReader;
|
|
12
|
+
/**
|
|
13
|
+
* Dependency names from `[dependencies]`, `[dev-dependencies]`, `[build-dependencies]`, `[workspace.dependencies]`
|
|
14
|
+
* and target tables, and `[dependencies.name]` headers. A renamed dependency reports its alias, not `package`. TOML puts each key on its own line, so a line scan is enough.
|
|
15
|
+
*/
|
|
16
|
+
export declare const cargoToml: ManifestReader;
|
|
9
17
|
export declare function manifests(): Plugin;
|
|
10
18
|
//#endregion
|
package/dist/manifests.mjs
CHANGED
|
@@ -34,6 +34,67 @@ const gemfile = {
|
|
|
34
34
|
file: "Gemfile",
|
|
35
35
|
read: (text) => [...text.matchAll(/^\s*gem\s+["']([^"']+)["']/gm)].map((match) => match[1])
|
|
36
36
|
};
|
|
37
|
+
/** The PEP 503 normalized name of a PEP 508 requirement: `Flask_Babel[async]>=4` is `flask-babel`. */
|
|
38
|
+
function requirementName(requirement) {
|
|
39
|
+
const name = /^\s*([a-z\d][\w.-]*)/i.exec(requirement)?.[1];
|
|
40
|
+
return name ? [name.toLowerCase().replace(/[-_.]+/g, "-")] : [];
|
|
41
|
+
}
|
|
42
|
+
/** TOML strings outside comments, up to the first `]` outside a string (`"i18nice[YAML]"` has none) when `toClose`. */
|
|
43
|
+
function tomlStrings(text, toClose) {
|
|
44
|
+
const out = [];
|
|
45
|
+
for (const match of text.matchAll(/"([^"\n]*)"|'([^'\n]*)'|#.*|(\])/g)) {
|
|
46
|
+
if (match[3] && toClose) break;
|
|
47
|
+
const value = match[1] ?? match[2];
|
|
48
|
+
if (value !== void 0) out.push(value);
|
|
49
|
+
}
|
|
50
|
+
return out;
|
|
51
|
+
}
|
|
52
|
+
/** Reads the PEP 621, PEP 735 and Poetry dependency tables; dependency names need no full TOML parser. */
|
|
53
|
+
const pyprojectToml = {
|
|
54
|
+
id: "pyproject.toml",
|
|
55
|
+
file: "pyproject.toml",
|
|
56
|
+
read: (text) => text.replace(/"""[\s\S]*?"""|'''[\s\S]*?'''/g, "\"\"").split(/^[ \t]*\[/m).flatMap((table) => {
|
|
57
|
+
const header = /^\[?([^\]\n]+)\]/.exec(table)?.[1]?.trim();
|
|
58
|
+
const body = table.slice(table.indexOf("\n") + 1);
|
|
59
|
+
if (header === "project") {
|
|
60
|
+
const start = /^\s*dependencies\s*=\s*\[/m.exec(body);
|
|
61
|
+
return start ? tomlStrings(body.slice(start.index + start[0].length), true).flatMap(requirementName) : [];
|
|
62
|
+
}
|
|
63
|
+
if (header === "project.optional-dependencies" || header === "dependency-groups") return tomlStrings(body, false).flatMap(requirementName);
|
|
64
|
+
if (header && /^tool\.poetry\.(?:group\.[\w-]+\.)?(?:dev-)?dependencies$/.test(header)) return [...body.matchAll(/^\s*(?:"([^"\n]+)"|'([^'\n]+)'|([\w-]+))\s*[.=]/gm)].flatMap((match) => requirementName(match[1] ?? match[2] ?? match[3])).filter((name) => name !== "python");
|
|
65
|
+
return [];
|
|
66
|
+
})
|
|
67
|
+
};
|
|
68
|
+
const requirementsTxt = {
|
|
69
|
+
id: "requirements.txt",
|
|
70
|
+
file: "requirements.txt",
|
|
71
|
+
read: (text) => text.split("\n").flatMap((line) => line.trim().startsWith("-") ? [] : requirementName(line))
|
|
72
|
+
};
|
|
73
|
+
const cargoSection = /^\[\s*((?:workspace\.|target\.[^\]]+\.)?(?:dev-|build-)?dependencies)(?:\.([\w-]+))?\s*\]/;
|
|
74
|
+
const cargoKey = /^\s*"?([\w-]+)"?\s*[.=]/;
|
|
75
|
+
/**
|
|
76
|
+
* Dependency names from `[dependencies]`, `[dev-dependencies]`, `[build-dependencies]`, `[workspace.dependencies]`
|
|
77
|
+
* and target tables, and `[dependencies.name]` headers. A renamed dependency reports its alias, not `package`. TOML puts each key on its own line, so a line scan is enough.
|
|
78
|
+
*/
|
|
79
|
+
const cargoToml = {
|
|
80
|
+
id: "Cargo.toml",
|
|
81
|
+
file: "Cargo.toml",
|
|
82
|
+
read(text) {
|
|
83
|
+
const names = [];
|
|
84
|
+
let inDependencies = false;
|
|
85
|
+
for (const line of text.split("\n")) {
|
|
86
|
+
if (line.trimStart().startsWith("[")) {
|
|
87
|
+
const section = cargoSection.exec(line.trim());
|
|
88
|
+
inDependencies = section !== null && section[2] === void 0;
|
|
89
|
+
if (section?.[2]) names.push(section[2]);
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
const name = inDependencies ? cargoKey.exec(line)?.[1] : void 0;
|
|
93
|
+
if (name) names.push(name);
|
|
94
|
+
}
|
|
95
|
+
return names;
|
|
96
|
+
}
|
|
97
|
+
};
|
|
37
98
|
function manifests() {
|
|
38
99
|
return {
|
|
39
100
|
name: "x18n:manifests",
|
|
@@ -41,9 +102,12 @@ function manifests() {
|
|
|
41
102
|
packageJson,
|
|
42
103
|
composerJson,
|
|
43
104
|
pubspecYaml,
|
|
44
|
-
gemfile
|
|
105
|
+
gemfile,
|
|
106
|
+
pyprojectToml,
|
|
107
|
+
requirementsTxt,
|
|
108
|
+
cargoToml
|
|
45
109
|
]
|
|
46
110
|
};
|
|
47
111
|
}
|
|
48
112
|
//#endregion
|
|
49
|
-
export { composerJson, gemfile, manifests, packageJson, pubspecYaml };
|
|
113
|
+
export { cargoToml, composerJson, gemfile, manifests, packageJson, pubspecYaml, pyprojectToml, requirementsTxt };
|
package/dist/placeholders.d.mts
CHANGED
|
@@ -2,12 +2,21 @@ import { PlaceholderSyntax, Plugin } from "@x18n/core";
|
|
|
2
2
|
//#region src/placeholders/index.d.ts
|
|
3
3
|
/** `{name}`: vue-i18n named interpolation and similar. */
|
|
4
4
|
export declare const braces: PlaceholderSyntax;
|
|
5
|
+
/** `{}`: easy_localization positional arguments. */
|
|
6
|
+
export declare const positionalBraces: PlaceholderSyntax;
|
|
7
|
+
/** `{name}` with an optional type, formatters or switch-case: `{name:string}`, `{date|weekday}`, `{gender|{male: his, *: their}}`. `{{a|b}}` plurals are not placeholders. */
|
|
8
|
+
export declare const typedBraces: PlaceholderSyntax;
|
|
5
9
|
/** `{{name}}`: i18next interpolation, including `{{name, format}}` and `{{- raw}}`. */
|
|
6
10
|
export declare const doubleBraces: PlaceholderSyntax;
|
|
7
11
|
/** `%{name}`: Rails, Polyglot. */
|
|
8
12
|
export declare const percentBraces: PlaceholderSyntax;
|
|
9
13
|
/** `%s`, `%d`, `%1$s`: printf. */
|
|
10
14
|
export declare const printf: PlaceholderSyntax;
|
|
15
|
+
/**
|
|
16
|
+
* `%(name)s`, `%s`, `%d`: Python percent formatting, as in gettext catalogs. No space flag, so `50% of` is text.
|
|
17
|
+
* URL escapes are text too: those with an uppercase hex letter (`%2F`), and any `%XX` after `://` in the same word (`%20a`).
|
|
18
|
+
*/
|
|
19
|
+
export declare const pythonPercent: PlaceholderSyntax;
|
|
11
20
|
/** `@:key` and `@.modifier:key`: vue-i18n linked messages. */
|
|
12
21
|
export declare const linked: PlaceholderSyntax;
|
|
13
22
|
/** `{ $name }`: Fluent variables. */
|
|
@@ -16,6 +25,8 @@ export declare const fluent: PlaceholderSyntax;
|
|
|
16
25
|
export declare const colon: PlaceholderSyntax;
|
|
17
26
|
/** `$name$`: Chrome extension messages. */
|
|
18
27
|
export declare const dollars: PlaceholderSyntax;
|
|
28
|
+
/** `{0}`, `{0:C}`, `{0,-10}`, `{name}`: .NET composite format items. `{{` and `}}` are literal braces. */
|
|
29
|
+
export declare const dotnet: PlaceholderSyntax;
|
|
19
30
|
/** `$t(key)` nesting in i18next. */
|
|
20
31
|
export declare const nesting: PlaceholderSyntax;
|
|
21
32
|
/** ICU MessageFormat arguments: `{name}`, `{count, plural, ...}`. Nested sub-messages are scanned too. */
|
package/dist/placeholders.mjs
CHANGED
|
@@ -20,12 +20,21 @@ function byRegex(id, regex, example) {
|
|
|
20
20
|
}
|
|
21
21
|
/** `{name}`: vue-i18n named interpolation and similar. */
|
|
22
22
|
const braces = byRegex("braces", /(?<![{$])\{\s*([\w.-]+)\s*\}(?!\})/g, "{name}");
|
|
23
|
+
/** `{}`: easy_localization positional arguments. */
|
|
24
|
+
const positionalBraces = byRegex("positional-braces", /\{\}/g, "{}");
|
|
25
|
+
/** `{name}` with an optional type, formatters or switch-case: `{name:string}`, `{date|weekday}`, `{gender|{male: his, *: their}}`. `{{a|b}}` plurals are not placeholders. */
|
|
26
|
+
const typedBraces = byRegex("typed-braces", /(?<![{$])\{\s*(\w+)\??\s*(?::[^{}|]*)?(?:\|(?:\{[^{}]*\}|[^{}]*))?\}(?!\})/g, "{name}");
|
|
23
27
|
/** `{{name}}`: i18next interpolation, including `{{name, format}}` and `{{- raw}}`. */
|
|
24
28
|
const doubleBraces = byRegex("double-braces", /\{\{-?\s*([\w.]+)\s*(?:,[^}]*)?\}\}/g, "{{name}}");
|
|
25
29
|
/** `%{name}`: Rails, Polyglot. */
|
|
26
30
|
const percentBraces = byRegex("percent-braces", /%\{(\w+)\}/g, "%{name}");
|
|
27
31
|
/** `%s`, `%d`, `%1$s`: printf. */
|
|
28
32
|
const printf = byRegex("printf", /%(?:(\d+)\$)?[-+ 0#]*(?:[1-9]\d*)?(?:\.\d+)?[sdifoxXeEgGc@]/g, "%s");
|
|
33
|
+
/**
|
|
34
|
+
* `%(name)s`, `%s`, `%d`: Python percent formatting, as in gettext catalogs. No space flag, so `50% of` is text.
|
|
35
|
+
* URL escapes are text too: those with an uppercase hex letter (`%2F`), and any `%XX` after `://` in the same word (`%20a`).
|
|
36
|
+
*/
|
|
37
|
+
const pythonPercent = byRegex("python-percent", /(?<!%)%(?![\dA-F][A-F])(?!(?<=:\/\/\S*%)[\dA-Fa-f]{2})(?:\((\w+)\))?[-#0+]*(?:[1-9]\d*|\*)?(?:\.\d+)?[c-giouxXEFGrsa]/g, "%(name)s");
|
|
29
38
|
/** `@:key` and `@.modifier:key`: vue-i18n linked messages. */
|
|
30
39
|
const linked = {
|
|
31
40
|
...byRegex("linked", /@(?:\.\w+)?:\(?([\w.-]+)\)?/g, "@:key"),
|
|
@@ -37,6 +46,24 @@ const fluent = byRegex("fluent", /\{\s*\$([\w-]+)(?:\s[^}]*)?\}/g, "{ $name }");
|
|
|
37
46
|
const colon = byRegex("colon", /(?<![\w:]):([a-z_]\w*)/gi, ":name");
|
|
38
47
|
/** `$name$`: Chrome extension messages. */
|
|
39
48
|
const dollars = byRegex("dollars", /\$(\w+)\$/g, "$name$");
|
|
49
|
+
/** `{0}`, `{0:C}`, `{0,-10}`, `{name}`: .NET composite format items. `{{` and `}}` are literal braces. */
|
|
50
|
+
const dotnet = {
|
|
51
|
+
id: "dotnet",
|
|
52
|
+
example: "{0}",
|
|
53
|
+
find(value) {
|
|
54
|
+
const out = [];
|
|
55
|
+
for (const match of value.matchAll(/\{\{|\}\}|\{\s*(\w+)\s*(?:,\s*-?\d+\s*)?(?::[^{}]*)?\}/g)) if (match[1] !== void 0) out.push({
|
|
56
|
+
syntax: "dotnet",
|
|
57
|
+
text: match[0],
|
|
58
|
+
name: match[1],
|
|
59
|
+
range: {
|
|
60
|
+
start: match.index,
|
|
61
|
+
end: match.index + match[0].length
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
return out;
|
|
65
|
+
}
|
|
66
|
+
};
|
|
40
67
|
/** `$t(key)` nesting in i18next. */
|
|
41
68
|
const nesting = {
|
|
42
69
|
...byRegex("nesting", /\$t\(([\w.:-]+)(?:,[^)]*)?\)/g, "$t(key)"),
|
|
@@ -128,4 +155,4 @@ function placeholderFromShape(shape) {
|
|
|
128
155
|
return byRegex(`shape:${shape}`, new RegExp(`${escape(before ?? "")}\\s*([\\w.-]+)\\s*${escape(after ?? "")}`, "g"), shape);
|
|
129
156
|
}
|
|
130
157
|
//#endregion
|
|
131
|
-
export { braces, colon, dollars, doubleBraces, fluent, icu, linked, nesting, percentBraces, placeholderFromShape, placeholders, printf };
|
|
158
|
+
export { braces, colon, dollars, dotnet, doubleBraces, fluent, icu, linked, nesting, percentBraces, placeholderFromShape, placeholders, positionalBraces, printf, pythonPercent, typedBraces };
|