@x18n/builtins 0.0.12 → 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 (170) hide show
  1. package/dist/angular-templates-DqIOGfzT.mjs +8 -0
  2. package/dist/checks.mjs +3 -2
  3. package/dist/extractors.d.mts +2 -2
  4. package/dist/extractors.mjs +18 -6
  5. package/dist/finders.d.mts +4 -1
  6. package/dist/finders.mjs +1 -1
  7. package/dist/formats/ecmascript.d.mts +5 -0
  8. package/dist/formats/ecmascript.mjs +211 -0
  9. package/dist/formats/ftl.d.mts +5 -0
  10. package/dist/formats/ftl.mjs +234 -0
  11. package/dist/formats/ini.d.mts +5 -0
  12. package/dist/formats/ini.mjs +158 -0
  13. package/dist/formats/json.d.mts +6 -0
  14. package/dist/formats/json.mjs +10 -0
  15. package/dist/formats/json5.d.mts +5 -0
  16. package/dist/formats/json5.mjs +30 -0
  17. package/dist/formats/jsonc.d.mts +6 -0
  18. package/dist/formats/jsonc.mjs +10 -0
  19. package/dist/formats/php.d.mts +5 -0
  20. package/dist/formats/php.mjs +200 -0
  21. package/dist/formats/po.d.mts +5 -0
  22. package/dist/formats/po.mjs +177 -0
  23. package/dist/formats/properties.d.mts +5 -0
  24. package/dist/formats/properties.mjs +123 -0
  25. package/dist/formats/resx.d.mts +6 -0
  26. package/dist/formats/resx.mjs +202 -0
  27. package/dist/formats/yaml.d.mts +5 -0
  28. package/dist/formats/yaml.mjs +113 -0
  29. package/dist/formats.d.mts +10 -34
  30. package/dist/formats.mjs +61 -1
  31. package/dist/index.d.mts +2 -2
  32. package/dist/index.mjs +28 -22
  33. package/dist/{vue-5GFR5hft.mjs → js-CV-ECNJG.mjs} +133 -273
  34. package/dist/json-CyjI8bjp.d.mts +8 -0
  35. package/dist/json-Db-8qSbE.mjs +73 -0
  36. package/dist/jsx-templates-B1J0no3i.mjs +17 -0
  37. package/dist/locale-root-C_rch3IQ.mjs +15 -0
  38. package/dist/manifests.d.mts +8 -0
  39. package/dist/manifests.mjs +66 -2
  40. package/dist/placeholders.d.mts +11 -0
  41. package/dist/placeholders.mjs +28 -1
  42. package/dist/plugin-Cb1b0axD.mjs +83 -0
  43. package/dist/plural-forms-B7_QX4nG.mjs +12 -0
  44. package/dist/presets/cargo-i18n.d.mts +6 -0
  45. package/dist/presets/cargo-i18n.mjs +28 -0
  46. package/dist/presets/chrome-extension.d.mts +6 -0
  47. package/dist/presets/chrome-extension.mjs +52 -0
  48. package/dist/presets/dotnet-resx.d.mts +6 -0
  49. package/dist/presets/dotnet-resx.mjs +64 -0
  50. package/dist/presets/easy-localization.d.mts +6 -0
  51. package/dist/presets/easy-localization.mjs +36 -0
  52. package/dist/presets/ember-intl.d.mts +5 -0
  53. package/dist/presets/ember-intl.mjs +31 -0
  54. package/dist/presets/fluent-react.d.mts +5 -0
  55. package/dist/presets/fluent-react.mjs +46 -0
  56. package/dist/presets/fluent-vue.d.mts +5 -0
  57. package/dist/presets/fluent-vue.mjs +51 -0
  58. package/dist/presets/flutter-translate.d.mts +6 -0
  59. package/dist/presets/flutter-translate.mjs +30 -0
  60. package/dist/presets/flutter.d.mts +6 -0
  61. package/dist/presets/flutter.mjs +21 -0
  62. package/dist/presets/general.d.mts +6 -0
  63. package/dist/presets/general.mjs +17 -0
  64. package/dist/presets/globalize.d.mts +5 -0
  65. package/dist/presets/globalize.mjs +25 -0
  66. package/dist/presets/gravity-ui-i18n.d.mts +6 -0
  67. package/dist/presets/gravity-ui-i18n.mjs +41 -0
  68. package/dist/presets/i18n-js.d.mts +5 -0
  69. package/dist/presets/i18n-js.mjs +19 -0
  70. package/dist/presets/i18n-tag.d.mts +5 -0
  71. package/dist/presets/i18n-tag.mjs +21 -0
  72. package/dist/presets/i18next-shopify.d.mts +6 -0
  73. package/dist/presets/i18next-shopify.mjs +23 -0
  74. package/dist/presets/i18next.d.mts +5 -0
  75. package/dist/presets/i18next.mjs +10 -0
  76. package/dist/presets/i18nice.d.mts +6 -0
  77. package/dist/presets/i18nice.mjs +29 -0
  78. package/dist/presets/jekyll.d.mts +5 -0
  79. package/dist/presets/jekyll.mjs +23 -0
  80. package/dist/presets/laravel-vue-i18n.d.mts +6 -0
  81. package/dist/presets/laravel-vue-i18n.mjs +54 -0
  82. package/dist/presets/laravel.d.mts +6 -0
  83. package/dist/presets/laravel.mjs +23 -0
  84. package/dist/presets/lingui.d.mts +5 -0
  85. package/dist/presets/lingui.mjs +61 -0
  86. package/dist/presets/next-international.d.mts +5 -0
  87. package/dist/presets/next-international.mjs +27 -0
  88. package/dist/presets/next-intl.d.mts +5 -0
  89. package/dist/presets/next-intl.mjs +55 -0
  90. package/dist/presets/next-translate.d.mts +5 -0
  91. package/dist/presets/next-translate.mjs +53 -0
  92. package/dist/presets/ngx-translate.d.mts +5 -0
  93. package/dist/presets/ngx-translate.mjs +41 -0
  94. package/dist/presets/nuxt-i18n-micro.d.mts +5 -0
  95. package/dist/presets/nuxt-i18n-micro.mjs +81 -0
  96. package/dist/presets/nuxt-i18n.d.mts +5 -0
  97. package/dist/presets/nuxt-i18n.mjs +23 -0
  98. package/dist/presets/paraglide.d.mts +5 -0
  99. package/dist/presets/paraglide.mjs +38 -0
  100. package/dist/presets/php-gettext.d.mts +6 -0
  101. package/dist/presets/php-gettext.mjs +27 -0
  102. package/dist/presets/php-joomla.d.mts +6 -0
  103. package/dist/presets/php-joomla.mjs +22 -0
  104. package/dist/presets/polyglot.d.mts +5 -0
  105. package/dist/presets/polyglot.mjs +23 -0
  106. package/dist/presets/python-gettext.d.mts +6 -0
  107. package/dist/presets/python-gettext.mjs +44 -0
  108. package/dist/presets/react-i18next.d.mts +5 -0
  109. package/dist/presets/react-i18next.mjs +67 -0
  110. package/dist/presets/react-intl.d.mts +5 -0
  111. package/dist/presets/react-intl.mjs +47 -0
  112. package/dist/presets/ruby-rails.d.mts +7 -0
  113. package/dist/presets/ruby-rails.mjs +44 -0
  114. package/dist/presets/solid-primitives-i18n.d.mts +5 -0
  115. package/dist/presets/solid-primitives-i18n.mjs +29 -0
  116. package/dist/presets/svelte-i18n.d.mts +5 -0
  117. package/dist/presets/svelte-i18n.mjs +33 -0
  118. package/dist/presets/svelte-i18next.d.mts +5 -0
  119. package/dist/presets/svelte-i18next.mjs +27 -0
  120. package/dist/presets/transloco.d.mts +5 -0
  121. package/dist/presets/transloco.mjs +44 -0
  122. package/dist/presets/typesafe-i18n.d.mts +6 -0
  123. package/dist/presets/typesafe-i18n.mjs +61 -0
  124. package/dist/presets/ui5.d.mts +5 -0
  125. package/dist/presets/ui5.mjs +35 -0
  126. package/dist/presets/vscode-extension.d.mts +5 -0
  127. package/dist/presets/vscode-extension.mjs +53 -0
  128. package/dist/presets/vue-i18n.d.mts +5 -0
  129. package/dist/presets/vue-i18n.mjs +59 -0
  130. package/dist/presets.d.mts +7 -54
  131. package/dist/presets.mjs +346 -1
  132. package/dist/reporters/github.d.mts +6 -0
  133. package/dist/reporters/github.mjs +27 -0
  134. package/dist/reporters/json.d.mts +6 -0
  135. package/dist/reporters/json.mjs +24 -0
  136. package/dist/reporters/markdown.d.mts +6 -0
  137. package/dist/reporters/markdown.mjs +37 -0
  138. package/dist/reporters/pretty.d.mts +6 -0
  139. package/dist/reporters/pretty.mjs +64 -0
  140. package/dist/reporters/sarif.d.mts +6 -0
  141. package/dist/reporters/sarif.mjs +48 -0
  142. package/dist/reporters.d.mts +5 -12
  143. package/dist/reporters.mjs +8 -218
  144. package/dist/scanners/astro.d.mts +6 -0
  145. package/dist/scanners/astro.mjs +256 -0
  146. package/dist/scanners/js.d.mts +6 -0
  147. package/dist/scanners/js.mjs +33 -0
  148. package/dist/scanners/regex-scopes.d.mts +14 -0
  149. package/dist/scanners/regex-scopes.mjs +41 -0
  150. package/dist/scanners/regex.d.mts +9 -0
  151. package/dist/{regex-B9TBGgXC.mjs → scanners/regex.mjs} +2 -33
  152. package/dist/scanners/svelte.d.mts +6 -0
  153. package/dist/{scanners-B27IEH4_.mjs → scanners/svelte.mjs} +8 -23
  154. package/dist/scanners/vue.d.mts +6 -0
  155. package/dist/scanners/vue.mjs +172 -0
  156. package/dist/scanners.d.mts +9 -47
  157. package/dist/scanners.mjs +36 -4
  158. package/dist/sfc-blocks-DaGKDZsb.mjs +213 -0
  159. package/dist/shared-BWaXUmwA.mjs +70 -0
  160. package/dist/shared-D-tbsYmc.mjs +30 -0
  161. package/dist/shared-D4NJBKEP.mjs +17 -0
  162. package/dist/shared-Tq06G0gp.mjs +6 -0
  163. package/dist/shared-meYeVckh.mjs +11 -0
  164. package/dist/vue-compiler-0sfLIiiL.mjs +8 -0
  165. package/package.json +67 -3
  166. package/dist/formats-CFkM5co0.mjs +0 -1370
  167. package/dist/presets-DJrGl3HR.mjs +0 -1184
  168. package/dist/sfc-blocks-BIPqKcvd.mjs +0 -161
  169. package/dist/yaml-plugin.d.mts +0 -8
  170. package/dist/yaml-plugin.mjs +0 -68
@@ -1,30 +1,30 @@
1
- //#region src/scanners/js.ts
2
- const hardStringIgnoredAttributes = /* @__PURE__ */ new Set([
3
- "class",
4
- "className",
5
- "style",
6
- "key",
7
- "ref",
8
- "id",
9
- "href",
10
- "src",
11
- "type",
12
- "name",
13
- "for",
14
- "htmlFor",
15
- "role",
16
- "slot",
17
- "as",
18
- "to",
19
- "target",
20
- "rel",
21
- "lang",
22
- "xmlns",
23
- "viewBox",
24
- "d",
25
- "fill",
26
- "stroke"
1
+ //#region src/scanners/shared/js.ts
2
+ const textAttributeWords = /* @__PURE__ */ new Set([
3
+ "title",
4
+ "subtitle",
5
+ "label",
6
+ "text",
7
+ "placeholder",
8
+ "alt",
9
+ "content",
10
+ "description",
11
+ "caption",
12
+ "message",
13
+ "tooltip",
14
+ "hint",
15
+ "summary",
16
+ "heading",
17
+ "headline"
27
18
  ]);
19
+ /**
20
+ * Markup attributes whose values users read: those whose last word is a text word (`title`, `aria-label`,
21
+ * `confirmText`, `empty-message`), plus `extract.attributes`. Every other attribute holds code: classes,
22
+ * URLs, ids, SVG geometry, attributify utilities.
23
+ */
24
+ function isTextAttribute(name, extra = []) {
25
+ const last = name.replace(/([a-z\d])([A-Z])/g, "$1-$2").toLowerCase().split(/[-_:.]/).at(-1);
26
+ return textAttributeWords.has(last) || extra.includes(name);
27
+ }
28
28
  function isNode(value) {
29
29
  return typeof value === "object" && value !== null && "type" in value && typeof value.type === "string" && "start" in value;
30
30
  }
@@ -73,6 +73,7 @@ var FileScan = class {
73
73
  offset;
74
74
  collectHardStrings;
75
75
  ignoredCallees;
76
+ textAttributes;
76
77
  usages = [];
77
78
  keySlots = [];
78
79
  hardStrings = [];
@@ -82,12 +83,17 @@ var FileScan = class {
82
83
  declared = /* @__PURE__ */ new Map();
83
84
  factories = /* @__PURE__ */ new Map();
84
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();
85
88
  consumed = /* @__PURE__ */ new Set();
86
- constructor(text, offset, calls, collectHardStrings, ignoredCallees = /* @__PURE__ */ new Set()) {
89
+ /** Render-prop functions of factory components (`<Translation>{t => ...}</Translation>`), by start offset. */
90
+ renderProps = /* @__PURE__ */ new Map();
91
+ constructor(text, offset, calls, collectHardStrings, ignoredCallees = /* @__PURE__ */ new Set(), textAttributes = []) {
87
92
  this.text = text;
88
93
  this.offset = offset;
89
94
  this.collectHardStrings = collectHardStrings;
90
95
  this.ignoredCallees = ignoredCallees;
96
+ this.textAttributes = textAttributes;
91
97
  for (const spec of calls) for (const name of [spec.callee].flat()) if (spec.factory) {
92
98
  const list = this.factories.get(name) ?? [];
93
99
  this.factories.set(name, list);
@@ -141,6 +147,11 @@ var FileScan = class {
141
147
  return constant && isStringLiteral(constant) ? constant.value : void 0;
142
148
  }
143
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
+ }
144
155
  argValues(args, source, fallback) {
145
156
  const arg = args[typeof source === "number" ? source : source?.arg ?? fallback];
146
157
  const property = typeof source === "object" ? source.property : void 0;
@@ -160,18 +171,17 @@ var FileScan = class {
160
171
  return object.properties.flatMap((p) => p.type === "Property" ? [p.value] : []);
161
172
  }
162
173
  /** ---- Usages ---- */
163
- addKey(value, binding, namespace) {
174
+ /** `expression` is the call holding `value`, kept only when the key is the call's own literal. */
175
+ addKey(value, base, expression) {
164
176
  if ("props" in value) return;
165
177
  const node = unwrap(value);
166
- const base = {
167
- namespace: namespace ?? binding.namespace,
168
- keyPrefix: binding.keyPrefix
169
- };
178
+ const call = expression ? { expression: this.range(expression) } : {};
170
179
  if (isStringLiteral(node)) {
171
180
  this.consumed.add(node.start);
172
181
  this.keySlots.push(this.range(innerRange(node)));
173
182
  this.usages.push({
174
183
  ...base,
184
+ ...call,
175
185
  raw: node.value,
176
186
  range: this.range(innerRange(node)),
177
187
  kind: "call",
@@ -180,7 +190,7 @@ var FileScan = class {
180
190
  return;
181
191
  }
182
192
  if (node.type === "JSXExpressionContainer") {
183
- if (node.expression.type !== "JSXEmptyExpression") this.addKey(node.expression, binding, namespace);
193
+ if (node.expression.type !== "JSXEmptyExpression") this.addKey(node.expression, base);
184
194
  return;
185
195
  }
186
196
  if (node.type === "TemplateLiteral") {
@@ -193,6 +203,7 @@ var FileScan = class {
193
203
  });
194
204
  this.usages.push({
195
205
  ...base,
206
+ ...call,
196
207
  raw,
197
208
  range,
198
209
  kind: "call",
@@ -201,8 +212,8 @@ var FileScan = class {
201
212
  return;
202
213
  }
203
214
  if (node.type === "ConditionalExpression") {
204
- this.addKey(node.consequent, binding, namespace);
205
- this.addKey(node.alternate, binding, namespace);
215
+ this.addKey(node.consequent, base);
216
+ this.addKey(node.alternate, base);
206
217
  return;
207
218
  }
208
219
  if (node.type === "ArrowFunctionExpression" && node.params[0]?.type === "Identifier") return this.addSelector(node.params[0].name, node.body, base);
@@ -217,6 +228,10 @@ var FileScan = class {
217
228
  });
218
229
  return;
219
230
  }
231
+ this.addUnknown(node, base);
232
+ }
233
+ addUnknown(node, base) {
234
+ if (isStringLiteral(node)) this.consumed.add(node.start);
220
235
  this.usages.push({
221
236
  ...base,
222
237
  raw: this.text.slice(node.start, node.end),
@@ -256,30 +271,49 @@ var FileScan = class {
256
271
  const object = this.constants.get(node.object.name);
257
272
  return object && object.type === "ObjectExpression" ? objectProperty(object, node.property.name) : void 0;
258
273
  }
259
- handleCall(name, args) {
274
+ handleCall(name, args, expression) {
260
275
  const bindings = this.lookupAll(name).filter((binding) => !binding.spec.factory);
261
276
  for (const binding of bindings) {
262
- 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
+ };
263
283
  for (const value of this.argValues(args, binding.spec.key, 0)) {
264
- if (bindings.length > 1 && !("props" in value) && unwrap(value).type === "ObjectExpression") continue;
265
- this.addKey(value, binding, namespace);
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);
266
288
  }
267
289
  }
268
290
  return bindings.length > 0;
269
291
  }
270
292
  call(node) {
271
293
  const name = calleeName(node.callee);
272
- if (name !== void 0) this.handleCall(name, node.arguments);
294
+ if (name !== void 0) this.handleCall(name, node.arguments, {
295
+ start: node.start,
296
+ end: node.end
297
+ });
273
298
  }
274
- jsx(node) {
275
- const name = jsxName(node.name);
276
- if (!name || !this.lookup(name)) return;
299
+ jsxProps(node) {
277
300
  const props = /* @__PURE__ */ new Map();
278
301
  for (const attribute of node.attributes) if (attribute.type === "JSXAttribute" && attribute.value && attribute.name.type === "JSXIdentifier") {
279
302
  props.set(attribute.name.name, attribute.value);
280
303
  if (isStringLiteral(attribute.value)) this.consumed.add(attribute.value.start);
281
304
  }
282
- this.handleCall(name, [{ props }]);
305
+ return { props };
306
+ }
307
+ jsx(node) {
308
+ const name = jsxName(node.name);
309
+ if (name && this.lookup(name)) this.handleCall(name, [this.jsxProps(node)]);
310
+ }
311
+ jsxElement(node) {
312
+ const name = jsxName(node.openingElement.name);
313
+ const specs = name ? this.factories.get(name) : void 0;
314
+ if (!specs) return;
315
+ const binding = this.pickFactory([this.jsxProps(node.openingElement)], specs);
316
+ for (const child of node.children) if (child.type === "JSXExpressionContainer" && (child.expression.type === "ArrowFunctionExpression" || child.expression.type === "FunctionExpression")) this.renderProps.set(child.expression.start, binding);
283
317
  }
284
318
  /** ---- Bindings ---- */
285
319
  declarator(id, init) {
@@ -289,7 +323,7 @@ var FileScan = class {
289
323
  if (value.type === "CallExpression") {
290
324
  const factory = calleeName(value.callee);
291
325
  const specs = factory ? this.factories.get(factory) : void 0;
292
- if (specs) this.bindFactory(id, value, specs);
326
+ if (specs) this.bindFactory(id, value.arguments, specs);
293
327
  return;
294
328
  }
295
329
  if (id.type === "Identifier" && (value.type === "Identifier" || value.type === "MemberExpression")) {
@@ -299,10 +333,10 @@ var FileScan = class {
299
333
  }
300
334
  }
301
335
  /** The first factory spec whose namespace and prefix sources all resolve, else the first spec. */
302
- pickFactory(call, specs) {
336
+ pickFactory(args, specs) {
303
337
  const resolved = specs.map((spec) => {
304
- const namespace = spec.namespace ? this.stringArg(this.argValues(call.arguments, spec.namespace, 0)[0]) : void 0;
305
- const keyPrefix = spec.keyPrefix ? this.stringArg(this.argValues(call.arguments, spec.keyPrefix, 0)[0]) : void 0;
338
+ const namespace = this.namespaceArg(args, spec.namespace, 0);
339
+ const keyPrefix = spec.keyPrefix ? this.stringArg(this.argValues(args, spec.keyPrefix, 0)[0]) : void 0;
306
340
  const complete = (!spec.namespace || namespace !== void 0) && (!spec.keyPrefix || keyPrefix !== void 0);
307
341
  return {
308
342
  binding: {
@@ -318,8 +352,8 @@ var FileScan = class {
318
352
  });
319
353
  return (resolved.find((r) => r.complete) ?? resolved[0]).binding;
320
354
  }
321
- bindFactory(id, call, specs) {
322
- const binding = this.pickFactory(call, specs);
355
+ bindFactory(id, args, specs) {
356
+ const binding = this.pickFactory(args, specs);
323
357
  const returns = (binding.spec.returns ?? ["t"]).filter((name) => !name.includes("."));
324
358
  if (id.type === "Identifier") {
325
359
  this.bind(id.name, binding);
@@ -334,17 +368,49 @@ var FileScan = class {
334
368
  if (key && local.type === "Identifier" && this.declared.has(`${key}.t`)) this.bind(`${local.name}.t`, binding);
335
369
  }
336
370
  }
371
+ /** Parameters typed as a translate function (`t: TFunction<'ns'>`), also as destructured props. */
337
372
  params(params) {
338
373
  for (const param of params) {
339
374
  const target = param.type === "TSParameterProperty" ? param.parameter : param;
340
- if (target.type !== "Identifier" || !target.typeAnnotation) continue;
341
- const annotation = target.typeAnnotation.typeAnnotation;
342
- const typeName = annotation.type === "TSTypeReference" && annotation.typeName.type === "Identifier" ? annotation.typeName.name : void 0;
343
- const spec = typeName ? this.typed.get(typeName) : void 0;
344
- 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: {
345
403
  ...spec,
346
404
  types: void 0
347
- } });
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);
348
414
  }
349
415
  }
350
416
  importSpecifier(node) {
@@ -383,6 +449,9 @@ var FileScan = class {
383
449
  case "CallExpression":
384
450
  this.call(node);
385
451
  break;
452
+ case "JSXElement":
453
+ this.jsxElement(node);
454
+ break;
386
455
  case "JSXOpeningElement":
387
456
  this.jsx(node);
388
457
  break;
@@ -391,9 +460,12 @@ var FileScan = class {
391
460
  break;
392
461
  case "FunctionDeclaration":
393
462
  case "FunctionExpression":
394
- case "ArrowFunctionExpression":
463
+ case "ArrowFunctionExpression": {
395
464
  this.params(node.params);
465
+ const binding = this.renderProps.get(node.start);
466
+ if (binding && node.params[0]?.type === "Identifier") this.bind(node.params[0].name, binding);
396
467
  break;
468
+ }
397
469
  case "ImportSpecifier":
398
470
  this.importSpecifier(node);
399
471
  break;
@@ -413,7 +485,7 @@ var FileScan = class {
413
485
  if (this.consumed.has(node.start) || !parent) return;
414
486
  if (parent.type === "JSXAttribute") {
415
487
  const name = parent.name.type === "JSXIdentifier" ? parent.name.name : "";
416
- if (isStringLiteral(node) && !hardStringIgnoredAttributes.has(name) && !name.startsWith("data-") && !name.startsWith("aria-")) this.hard(node.value, {
488
+ if (isStringLiteral(node) && isTextAttribute(name, this.textAttributes)) this.hard(node.value, {
417
489
  start: parent.start,
418
490
  end: parent.end
419
491
  }, "jsx-attribute", name);
@@ -505,223 +577,11 @@ function scanSource(parse, text, options) {
505
577
  sourceType: "module",
506
578
  range: false
507
579
  });
508
- const scan = new FileScan(source, (options.offset ?? 0) + shift, options.calls, options.hardStrings !== false, new Set(options.ignoreCallees));
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);
509
582
  scan.walk(result.program, void 0, "");
510
583
  scan.markers(result.program, result.comments);
511
584
  return scan.result();
512
585
  }
513
- /** The AST scanner for JS and TS, driven by call specs (`usage.calls`). */
514
- function js() {
515
- return {
516
- name: "x18n:scanner-js",
517
- scanners: [{
518
- id: "js",
519
- extensions: [
520
- "js",
521
- "jsx",
522
- "mjs",
523
- "cjs",
524
- "ts",
525
- "tsx",
526
- "mts",
527
- "cts"
528
- ],
529
- async scan(file, ctx) {
530
- return scanSource(await loadParser(), file.text, {
531
- calls: ctx.calls,
532
- filename: file.path,
533
- hardStrings: true,
534
- ignoreCallees: ctx.config.extract.ignoreCallees
535
- });
536
- }
537
- }]
538
- };
539
- }
540
- //#endregion
541
- //#region src/scanners/vue.ts
542
- let compiler;
543
- function loadVueCompiler() {
544
- compiler ??= import("@vue/compiler-sfc");
545
- return compiler;
546
- }
547
- const ELEMENT = 1;
548
- const TEXT = 2;
549
- const SIMPLE_EXPRESSION = 4;
550
- const INTERPOLATION = 5;
551
- const ATTRIBUTE = 6;
552
- /** `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. */
553
- function isKeyAttribute(tag, name) {
554
- return name === "keypath" || name === "path" && (tag === "i18n" || tag === "i18n-d");
555
- }
556
- const ignoredAttributes = /* @__PURE__ */ new Set([
557
- "class",
558
- "style",
559
- "key",
560
- "ref",
561
- "id",
562
- "name",
563
- "type",
564
- "href",
565
- "src",
566
- "to",
567
- "for",
568
- "role",
569
- "lang",
570
- "target",
571
- "rel",
572
- "is",
573
- "slot"
574
- ]);
575
- function merge(into, result) {
576
- into.usages.push(...result.usages);
577
- into.keySlots.push(...result.keySlots ?? []);
578
- into.hardStrings.push(...result.hardStrings ?? []);
579
- }
580
- var TemplateScan = class {
581
- parse;
582
- calls;
583
- out = {
584
- usages: [],
585
- keySlots: [],
586
- hardStrings: []
587
- };
588
- constructor(parse, calls) {
589
- this.parse = parse;
590
- this.calls = calls;
591
- }
592
- expression(content, offset) {
593
- if (!content.trim()) return;
594
- try {
595
- merge(this.out, scanSource(this.parse, content, {
596
- calls: this.calls,
597
- offset,
598
- filename: "template.ts",
599
- expression: true,
600
- hardStrings: false
601
- }));
602
- } catch {}
603
- }
604
- /** `:keypath="cond ? 'a' : 'b'"`: the expression is read as the key argument of a translate call. */
605
- keyExpression(content, offset) {
606
- const callee = "__x18nKey(";
607
- try {
608
- merge(this.out, scanSource(this.parse, `${callee}${content})`, {
609
- calls: [{ callee: "__x18nKey" }],
610
- offset: offset - 10,
611
- filename: "template.ts",
612
- hardStrings: false
613
- }));
614
- } catch {}
615
- }
616
- /** `v-t="'key'"` and `v-t="{ path: 'key' }"`. */
617
- vt(dir) {
618
- const exp = dir.exp;
619
- if (!exp || !("content" in exp)) return;
620
- const offset = exp.loc.start.offset;
621
- const literal = /^\s*(['"`])(.*)\1\s*$/.exec(exp.content) ?? /path\s*:\s*(['"`])(.*?)\1/.exec(exp.content);
622
- if (!literal) return;
623
- const start = offset + exp.content.indexOf(literal[2], literal.index);
624
- const range = {
625
- start,
626
- end: start + literal[2].length
627
- };
628
- this.out.usages.push({
629
- raw: literal[2],
630
- range,
631
- kind: "call",
632
- dynamic: false
633
- });
634
- this.out.keySlots.push(range);
635
- }
636
- attribute(element, prop) {
637
- if (prop.type === ATTRIBUTE) {
638
- const value = prop.value;
639
- if (!value) return;
640
- const range = {
641
- start: value.loc.start.offset + 1,
642
- end: value.loc.end.offset - 1
643
- };
644
- if (isKeyAttribute(element.tag, prop.name)) {
645
- this.out.usages.push({
646
- raw: value.content,
647
- range,
648
- kind: "call",
649
- dynamic: false
650
- });
651
- this.out.keySlots.push(range);
652
- } else if (!ignoredAttributes.has(prop.name) && !prop.name.startsWith("data-") && /\p{L}/u.test(value.content)) this.out.hardStrings.push({
653
- text: value.content,
654
- range: {
655
- start: prop.loc.start.offset,
656
- end: prop.loc.end.offset
657
- },
658
- kind: "vue-attribute",
659
- attribute: prop.name
660
- });
661
- return;
662
- }
663
- if (prop.name === "t") {
664
- this.vt(prop);
665
- return;
666
- }
667
- const exp = prop.exp;
668
- if (exp && "content" in exp) {
669
- if (prop.name === "bind" && prop.arg && "content" in prop.arg && isKeyAttribute(element.tag, prop.arg.content)) this.keyExpression(exp.content, exp.loc.start.offset);
670
- else this.expression(exp.content, exp.loc.start.offset);
671
- }
672
- }
673
- node(node) {
674
- if (node.type === INTERPOLATION && "content" in node.content) this.expression(node.content.content, node.content.loc.start.offset);
675
- if (node.type === TEXT) {
676
- const text = node.content.trim();
677
- if (text && /\p{L}/u.test(text)) {
678
- const start = node.loc.start.offset + node.content.indexOf(text);
679
- this.out.hardStrings.push({
680
- text,
681
- range: {
682
- start,
683
- end: start + text.length
684
- },
685
- kind: "vue-template-text"
686
- });
687
- }
688
- }
689
- if (node.type === ELEMENT) for (const prop of node.props) this.attribute(node, prop);
690
- if ("children" in node) {
691
- for (const child of node.children) if (typeof child === "object" && child.type !== SIMPLE_EXPRESSION) this.node(child);
692
- }
693
- }
694
- };
695
- /** Vue SFCs: `<script>` through the `js` scanner, template expressions, `v-t`, `<i18n-t keypath>`. */
696
- function vue() {
697
- return {
698
- name: "x18n:scanner-vue",
699
- scanners: [{
700
- id: "vue",
701
- extensions: ["vue"],
702
- async scan(file, ctx) {
703
- const [{ parse }, parseJs] = await Promise.all([loadVueCompiler(), loadParser()]);
704
- const { descriptor } = parse(file.text, {
705
- filename: file.path,
706
- sourceMap: false,
707
- ignoreEmpty: false
708
- });
709
- const template = new TemplateScan(parseJs, ctx.calls);
710
- for (const script of [descriptor.script, descriptor.scriptSetup]) {
711
- if (!script) continue;
712
- const lang = script.lang ?? "js";
713
- merge(template.out, scanSource(parseJs, script.content, {
714
- calls: ctx.calls,
715
- ignoreCallees: ctx.config.extract.ignoreCallees,
716
- offset: script.loc.start.offset,
717
- filename: `script.${lang === "tsx" || lang === "jsx" ? lang : lang === "ts" ? "ts" : "js"}`
718
- }));
719
- }
720
- if (descriptor.template?.ast) template.node(descriptor.template.ast);
721
- return template.out;
722
- }
723
- }]
724
- };
725
- }
726
586
  //#endregion
727
- export { loadParser as a, js as i, vue as n, scanSource as o, calleeName as r, 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 };