@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.
Files changed (168) hide show
  1. package/dist/angular-templates-DqIOGfzT.mjs +8 -0
  2. package/dist/extractors.mjs +7 -3
  3. package/dist/finders.d.mts +4 -1
  4. package/dist/finders.mjs +1 -1
  5. package/dist/formats/ecmascript.d.mts +5 -0
  6. package/dist/formats/ecmascript.mjs +211 -0
  7. package/dist/formats/ftl.d.mts +5 -0
  8. package/dist/formats/ftl.mjs +234 -0
  9. package/dist/formats/ini.d.mts +5 -0
  10. package/dist/formats/ini.mjs +158 -0
  11. package/dist/formats/json.d.mts +6 -0
  12. package/dist/formats/json.mjs +10 -0
  13. package/dist/formats/json5.d.mts +5 -0
  14. package/dist/formats/json5.mjs +30 -0
  15. package/dist/formats/jsonc.d.mts +6 -0
  16. package/dist/formats/jsonc.mjs +10 -0
  17. package/dist/formats/php.d.mts +5 -0
  18. package/dist/formats/php.mjs +200 -0
  19. package/dist/formats/po.d.mts +5 -0
  20. package/dist/formats/po.mjs +177 -0
  21. package/dist/formats/properties.d.mts +5 -0
  22. package/dist/formats/properties.mjs +123 -0
  23. package/dist/formats/resx.d.mts +6 -0
  24. package/dist/formats/resx.mjs +202 -0
  25. package/dist/formats/yaml.d.mts +5 -0
  26. package/dist/formats/yaml.mjs +113 -0
  27. package/dist/formats.d.mts +10 -34
  28. package/dist/formats.mjs +61 -1
  29. package/dist/index.d.mts +2 -2
  30. package/dist/index.mjs +28 -22
  31. package/dist/{vue-CF4pe-Yr.mjs → js-CV-ECNJG.mjs} +67 -222
  32. package/dist/json-CyjI8bjp.d.mts +8 -0
  33. package/dist/json-Db-8qSbE.mjs +73 -0
  34. package/dist/jsx-templates-B1J0no3i.mjs +17 -0
  35. package/dist/locale-root-C_rch3IQ.mjs +15 -0
  36. package/dist/manifests.d.mts +8 -0
  37. package/dist/manifests.mjs +66 -2
  38. package/dist/placeholders.d.mts +11 -0
  39. package/dist/placeholders.mjs +28 -1
  40. package/dist/plugin-Cb1b0axD.mjs +83 -0
  41. package/dist/plural-forms-B7_QX4nG.mjs +12 -0
  42. package/dist/presets/cargo-i18n.d.mts +6 -0
  43. package/dist/presets/cargo-i18n.mjs +28 -0
  44. package/dist/presets/chrome-extension.d.mts +6 -0
  45. package/dist/presets/chrome-extension.mjs +52 -0
  46. package/dist/presets/dotnet-resx.d.mts +6 -0
  47. package/dist/presets/dotnet-resx.mjs +64 -0
  48. package/dist/presets/easy-localization.d.mts +6 -0
  49. package/dist/presets/easy-localization.mjs +36 -0
  50. package/dist/presets/ember-intl.d.mts +5 -0
  51. package/dist/presets/ember-intl.mjs +31 -0
  52. package/dist/presets/fluent-react.d.mts +5 -0
  53. package/dist/presets/fluent-react.mjs +46 -0
  54. package/dist/presets/fluent-vue.d.mts +5 -0
  55. package/dist/presets/fluent-vue.mjs +51 -0
  56. package/dist/presets/flutter-translate.d.mts +6 -0
  57. package/dist/presets/flutter-translate.mjs +30 -0
  58. package/dist/presets/flutter.d.mts +6 -0
  59. package/dist/presets/flutter.mjs +21 -0
  60. package/dist/presets/general.d.mts +6 -0
  61. package/dist/presets/general.mjs +17 -0
  62. package/dist/presets/globalize.d.mts +5 -0
  63. package/dist/presets/globalize.mjs +25 -0
  64. package/dist/presets/gravity-ui-i18n.d.mts +6 -0
  65. package/dist/presets/gravity-ui-i18n.mjs +41 -0
  66. package/dist/presets/i18n-js.d.mts +5 -0
  67. package/dist/presets/i18n-js.mjs +19 -0
  68. package/dist/presets/i18n-tag.d.mts +5 -0
  69. package/dist/presets/i18n-tag.mjs +21 -0
  70. package/dist/presets/i18next-shopify.d.mts +6 -0
  71. package/dist/presets/i18next-shopify.mjs +23 -0
  72. package/dist/presets/i18next.d.mts +5 -0
  73. package/dist/presets/i18next.mjs +10 -0
  74. package/dist/presets/i18nice.d.mts +6 -0
  75. package/dist/presets/i18nice.mjs +29 -0
  76. package/dist/presets/jekyll.d.mts +5 -0
  77. package/dist/presets/jekyll.mjs +23 -0
  78. package/dist/presets/laravel-vue-i18n.d.mts +6 -0
  79. package/dist/presets/laravel-vue-i18n.mjs +54 -0
  80. package/dist/presets/laravel.d.mts +6 -0
  81. package/dist/presets/laravel.mjs +23 -0
  82. package/dist/presets/lingui.d.mts +5 -0
  83. package/dist/presets/lingui.mjs +61 -0
  84. package/dist/presets/next-international.d.mts +5 -0
  85. package/dist/presets/next-international.mjs +27 -0
  86. package/dist/presets/next-intl.d.mts +5 -0
  87. package/dist/presets/next-intl.mjs +55 -0
  88. package/dist/presets/next-translate.d.mts +5 -0
  89. package/dist/presets/next-translate.mjs +53 -0
  90. package/dist/presets/ngx-translate.d.mts +5 -0
  91. package/dist/presets/ngx-translate.mjs +41 -0
  92. package/dist/presets/nuxt-i18n-micro.d.mts +5 -0
  93. package/dist/presets/nuxt-i18n-micro.mjs +81 -0
  94. package/dist/presets/nuxt-i18n.d.mts +5 -0
  95. package/dist/presets/nuxt-i18n.mjs +23 -0
  96. package/dist/presets/paraglide.d.mts +5 -0
  97. package/dist/presets/paraglide.mjs +38 -0
  98. package/dist/presets/php-gettext.d.mts +6 -0
  99. package/dist/presets/php-gettext.mjs +27 -0
  100. package/dist/presets/php-joomla.d.mts +6 -0
  101. package/dist/presets/php-joomla.mjs +22 -0
  102. package/dist/presets/polyglot.d.mts +5 -0
  103. package/dist/presets/polyglot.mjs +23 -0
  104. package/dist/presets/python-gettext.d.mts +6 -0
  105. package/dist/presets/python-gettext.mjs +44 -0
  106. package/dist/presets/react-i18next.d.mts +5 -0
  107. package/dist/presets/react-i18next.mjs +67 -0
  108. package/dist/presets/react-intl.d.mts +5 -0
  109. package/dist/presets/react-intl.mjs +47 -0
  110. package/dist/presets/ruby-rails.d.mts +7 -0
  111. package/dist/presets/ruby-rails.mjs +44 -0
  112. package/dist/presets/solid-primitives-i18n.d.mts +5 -0
  113. package/dist/presets/solid-primitives-i18n.mjs +29 -0
  114. package/dist/presets/svelte-i18n.d.mts +5 -0
  115. package/dist/presets/svelte-i18n.mjs +33 -0
  116. package/dist/presets/svelte-i18next.d.mts +5 -0
  117. package/dist/presets/svelte-i18next.mjs +27 -0
  118. package/dist/presets/transloco.d.mts +5 -0
  119. package/dist/presets/transloco.mjs +44 -0
  120. package/dist/presets/typesafe-i18n.d.mts +6 -0
  121. package/dist/presets/typesafe-i18n.mjs +61 -0
  122. package/dist/presets/ui5.d.mts +5 -0
  123. package/dist/presets/ui5.mjs +35 -0
  124. package/dist/presets/vscode-extension.d.mts +5 -0
  125. package/dist/presets/vscode-extension.mjs +53 -0
  126. package/dist/presets/vue-i18n.d.mts +5 -0
  127. package/dist/presets/vue-i18n.mjs +59 -0
  128. package/dist/presets.d.mts +7 -54
  129. package/dist/presets.mjs +346 -1
  130. package/dist/reporters/github.d.mts +6 -0
  131. package/dist/reporters/github.mjs +27 -0
  132. package/dist/reporters/json.d.mts +6 -0
  133. package/dist/reporters/json.mjs +24 -0
  134. package/dist/reporters/markdown.d.mts +6 -0
  135. package/dist/reporters/markdown.mjs +37 -0
  136. package/dist/reporters/pretty.d.mts +6 -0
  137. package/dist/reporters/pretty.mjs +64 -0
  138. package/dist/reporters/sarif.d.mts +6 -0
  139. package/dist/reporters/sarif.mjs +48 -0
  140. package/dist/reporters.d.mts +5 -12
  141. package/dist/reporters.mjs +8 -218
  142. package/dist/scanners/astro.d.mts +6 -0
  143. package/dist/scanners/astro.mjs +256 -0
  144. package/dist/scanners/js.d.mts +6 -0
  145. package/dist/scanners/js.mjs +33 -0
  146. package/dist/scanners/regex-scopes.d.mts +14 -0
  147. package/dist/scanners/regex-scopes.mjs +41 -0
  148. package/dist/scanners/regex.d.mts +9 -0
  149. package/dist/{regex-AYVhuSb6.mjs → scanners/regex.mjs} +2 -40
  150. package/dist/scanners/svelte.d.mts +6 -0
  151. package/dist/{scanners-DhnnVp7s.mjs → scanners/svelte.mjs} +3 -3
  152. package/dist/scanners/vue.d.mts +6 -0
  153. package/dist/scanners/vue.mjs +172 -0
  154. package/dist/scanners.d.mts +9 -50
  155. package/dist/scanners.mjs +36 -4
  156. package/dist/sfc-blocks-DaGKDZsb.mjs +213 -0
  157. package/dist/shared-BWaXUmwA.mjs +70 -0
  158. package/dist/shared-D-tbsYmc.mjs +30 -0
  159. package/dist/shared-D4NJBKEP.mjs +17 -0
  160. package/dist/shared-Tq06G0gp.mjs +6 -0
  161. package/dist/shared-meYeVckh.mjs +11 -0
  162. package/dist/vue-compiler-0sfLIiiL.mjs +8 -0
  163. package/package.json +67 -3
  164. package/dist/formats-CFkM5co0.mjs +0 -1370
  165. package/dist/presets-Co5SlnKr.mjs +0 -1187
  166. package/dist/sfc-blocks-BqQ8m9rX.mjs +0 -161
  167. package/dist/yaml-plugin.d.mts +0 -8
  168. 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, binding, namespace, expression) {
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, binding, namespace);
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, binding, namespace);
213
- this.addKey(node.alternate, binding, namespace);
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 = binding.spec.namespace ? this.stringArg(this.argValues(args, binding.spec.namespace, 1)[0]) : void 0;
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 (bindings.length > 1 && !("props" in value) && unwrap(value).type === "ObjectExpression") continue;
273
- this.addKey(value, binding, namespace, expression);
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 = spec.namespace ? this.stringArg(this.argValues(args, spec.namespace, 0)[0]) : void 0;
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
- if (target.type !== "Identifier" || !target.typeAnnotation) continue;
361
- const annotation = target.typeAnnotation.typeAnnotation;
362
- const typeName = annotation.type === "TSTypeReference" && annotation.typeName.type === "Identifier" ? annotation.typeName.name : void 0;
363
- const spec = typeName ? this.typed.get(typeName) : void 0;
364
- if (spec) this.bind(target.name, { spec: {
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 { js as a, isTextAttribute as i, vue as n, loadParser as o, calleeName as r, scanSource as s, loadVueCompiler as t };
587
+ export { loadParser as n, scanSource as r, isTextAttribute as t };
@@ -0,0 +1,8 @@
1
+ import "@x18n/core";
2
+ //#region src/formats/shared/json.d.ts
3
+ interface JsonOptions {
4
+ /** Spaces per level for new files. Existing files keep their indent. */
5
+ readonly indent?: number;
6
+ }
7
+ //#endregion
8
+ export { JsonOptions 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 };
@@ -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
@@ -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 };
@@ -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. */
@@ -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 };