wuchale 0.2.0 → 0.3.0

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/README.md CHANGED
@@ -26,6 +26,10 @@ simplicity in mind.
26
26
  No extra imports or annotations. `wuchale` extracts and compiles everything
27
27
  automatically. In the spirit of Svelte itself.
28
28
 
29
+ > They say "i18n is too costly to add later"
30
+
31
+ ✨ **Not anymore**. wuchale brings i18n to existing Svelte projects — the UX-first way.
32
+
29
33
  ### 🧠 Compiler-Powered
30
34
 
31
35
  Built on the Svelte compiler and powered by AST
@@ -127,7 +131,7 @@ Then finally you write your Svelte files naturally:
127
131
  <script>
128
132
  import WuchaleTrans, { wuchaleTrans } from 'wuchale/runtime.svelte'
129
133
  </script>
130
- <h1>{wuchaleTrans(0)}</h1> <!-- Extracted "Hello" as index 0 -->
134
+ <p>{wuchaleTrans(0)}</p> <!-- Extracted "Hello" as index 0 -->
131
135
  ```
132
136
 
133
137
  Full example below.
@@ -236,30 +240,88 @@ This is what you import when you set it up above in `App.svelte`.
236
240
 
237
241
  ## Supported syntax
238
242
 
239
- Text can be in three places: markup, script and attributes. Script means not
240
- just the part inside the `<script>` tags, but also inside interpolations inside
241
- the markup, such as `<p>{'This string'}</p>` and also in other places such as
242
- `{#if 'this string' == ''}`. And each can have their own rules. While these
243
- rules can be configured, the default is:
244
-
245
- - Markup:
246
- - All text should be extracted unless prefixed with `-`. Example: `<p>-
247
- This will be ignored.</p>`
248
- - Attributes:
249
- - All attributes starting with upper case letters are extracted unless
250
- prefixed with `-` like `label="-Ignore"`.
251
- - All attributes starting with lower case letters are ignored, unless
252
- prefixed with `+` like `alt="+extract"`.
253
- - Script:
254
- - Strings: same rules as attributes above, but:
255
- - If they are used inside the `<script>` tags, there is the additional
256
- restriction that they must be inside a `$derived` variable declaration.
257
- This is to make the behavior less magic and being more explicit.
258
-
259
- ## Where does it look?
260
-
261
- All files that can contain reactive logic. This means `*.svelte` and
262
- `*.svelte.js` files specifically.
243
+ Text can be in three places and the probability of the text inside them being
244
+ intended is different for each of them. Therefore, a global heuristic function
245
+ is applied to check whether the text should be extracted depending on each
246
+ case, discussed below. And for specific granular control, comments can be used,
247
+ like for typescript: `@wc-ignore` and `@wc-include`.
248
+
249
+ ### Markup
250
+
251
+ Markup means text that we write inside tags like in paragraphs. This is almost
252
+ always intended to be shown to the user. Therefore, the default global rule is
253
+ to extract all text inside the markup, with the ability to force ignore with a
254
+ comment:
255
+
256
+ ```svelte
257
+ <p>This is extracted</p>
258
+ <!-- @wc-ignore -->
259
+ <p>This is ignored</p>
260
+ ```
261
+
262
+ ### Attributes
263
+
264
+ Attributes are the text that are literally written like `class="this"`. Dynamic
265
+ attributes are not considered for this rule, instead they follow the script
266
+ rule below, because they are JS expressions. For text attributes, the default
267
+ rule is that all text starting with a capital letter is extracted.
268
+ Example:
269
+
270
+ ```svelte
271
+ <p class="not-extracted" title="Extracted">
272
+ This is extracted
273
+ </p>
274
+ ```
275
+
276
+ For ignoring or force-including, convert them to expressions and follow the
277
+ script rule below.
278
+
279
+ ### Script
280
+
281
+ This includes all JS/TS code that is:
282
+ - Inside `<script>` tags
283
+ - Inside dynamic expressions inside the markup or attributes, anything in curly braces like `{call('this guy')}`
284
+ - In `*.svelte.[js|ts]` files.
285
+
286
+ The rule for this is that all strings and template strings that start with
287
+ capital letters are extracted. Additionally, if they are used inside the
288
+ `<script>` tags and in their own files (third case above), there is the
289
+ additional restriction that they must be inside a `$derived` variable
290
+ declaration. This is to make the behavior less magic and being more explicit.
291
+ Example:
292
+
293
+ ```svelte
294
+ <script>
295
+ const a = 'Not extracted'
296
+ const b = $derived('not extracted either')
297
+ const c = $derived('This one is extracted')
298
+ const d = $derived(`This one as well ${a} - ${b}`)
299
+ const d = $derived(/* @wc-include */ `${a} - ${b} this is force extracted`)
300
+ </script>
301
+
302
+ <p class={/* @wc-ignore */ `Ignore${3}`} title={'Included'} >Normal text</p>
303
+
304
+ ```
305
+
306
+ ## Context?
307
+
308
+ Sometimes we need to have different translations that are the same text in the
309
+ source language. For that, the comment directive `@wc-context:` is provided and
310
+ they will be separate.
311
+
312
+ ```svelte
313
+ <b>
314
+ <!-- @wc-context: machine -->
315
+ Maintenance
316
+ </b>
317
+ <i>Is different from</i>
318
+ <b>
319
+ <!-- @wc-context: beauty -->
320
+ Maintenance
321
+ </b>
322
+ ```
323
+
324
+ Excuse my poor example choice.
263
325
 
264
326
  ## Plurals?
265
327
 
@@ -292,17 +354,14 @@ export const defaultOptions = {
292
354
  ```
293
355
 
294
356
  While the others are self explanatory, the `heuristic` is a function that
295
- decides what text to extract and what not to. The `defaultHeuristic` is the
296
- implementation of the above rules, but you can roll your own and provide it
297
- here. The function should receive the following arguments:
357
+ globally decides what text to extract and what not to. The `defaultHeuristic`
358
+ is the implementation of the above rules, but you can roll your own and provide
359
+ it here. The function should receive the following arguments:
298
360
 
299
361
  - `text`: The candidate text
300
362
  - `scope`: Where the text is located, i.e. it can be one of `markup`, `script`, and `attribute`
301
363
 
302
- And it should return an object with two properties:
303
-
304
- - `extract` (boolean): Whether to extract it or not
305
- - `replace` (`string`): The string to replace it with. This is how you specify how to remove parts such as the prefixes above (`+` and `-`).
364
+ And it should return boolean to indicate whether to extract it.
306
365
 
307
366
  ## 🧹 Cleaning
308
367
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wuchale",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "i18n for svelte without turning your codebase upside down",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -1,33 +1,40 @@
1
+ // $$ cd .. && npm run test
1
2
  // $$ node %f
2
3
 
4
+ import PO from 'pofile'
5
+
3
6
  const baseURL = 'https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key='
4
7
  const h = {'Content-Type': 'application/json'}
5
8
 
6
9
  /**
7
- * @param {string[]} fragments
10
+ * @param {import('pofile').Item[]} fragments
8
11
  * @param {string} sourceLocale
9
12
  * @param {string} targetLocale
10
13
  */
11
14
  function prepareData(fragments, sourceLocale, targetLocale) {
12
15
  const instruction = `
13
- You will be given text fragments for a web app separated by new line characters.
14
- Translate each of the fragments from the source to the target language.
16
+ You will be given the contents of a gettext .po file for a web app.
17
+ Translate each of the items from the source to the target language.
15
18
  You have to find out the languages using their ISO 639-1 codes.
16
19
  The source language is: ${sourceLocale}.
17
20
  The target language is: ${targetLocale}.
18
- Preserve any placeholders and provide the translations in the target language only,
19
- separated by new line characters in the same order.
21
+ You can read all of the information for the items including contexts,
22
+ comments and references to get the appropriate context about each item.
23
+ Provide the translated fragments in the in the same order, preserving
24
+ all placeholders.
20
25
  The placeholder format is like the following examples:
21
26
  - {0}: means arbitrary values.
22
27
  - <0>something</0>: means something enclosed in some tags, like HTML tags
23
28
  - <0/>: means a self closing tag, like in HTML
24
29
  In all of the examples, 0 is an example for any integer.
25
30
  `
31
+ const po = new PO()
32
+ po.items = fragments
26
33
  return {
27
34
  system_instruction: {
28
35
  parts: [{ text: instruction }]
29
36
  },
30
- contents: [{parts: [{text: fragments.join('\n')}]}]
37
+ contents: [{parts: [{text: po.toString()}]}]
31
38
  }
32
39
  }
33
40
 
@@ -43,18 +50,16 @@ function setupGemini(sourceLocale = 'en', targetLocale, apiKey) {
43
50
  return
44
51
  }
45
52
  const url = `${baseURL}${apiKey}`
46
- return async (/** @type {string[]} */ fragments) => {
53
+ return async (/** @type {import('pofile').Item[]} */ fragments) => {
47
54
  const data = prepareData(fragments, sourceLocale, targetLocale)
48
55
  const res = await fetch(url, {method: 'POST', headers: h, body: JSON.stringify(data)})
49
56
  const json = await res.json()
50
57
  const resText = json.candidates[0].content.parts[0].text
51
- const trans = {}
52
- for (const [i, text] of resText.split('\n').entries()) {
53
- if (text.trim()) {
54
- trans[fragments[i]] = text
58
+ for (const [i, item] of PO.parse(resText).items.entries()) {
59
+ if (item.msgstr[0]) {
60
+ fragments[i].msgstr = item.msgstr
55
61
  }
56
62
  }
57
- return trans
58
63
  }
59
64
  }
60
65
 
@@ -1,10 +1,10 @@
1
- import Preprocess, { defaultHeuristic, IndexTracker } from "./prep.js"
1
+ import Preprocess, { defaultHeuristic, IndexTracker, NestText } from "./prep.js"
2
2
  import { parse } from "svelte/compiler"
3
3
  import {writeFile} from 'node:fs/promises'
4
4
  import compileTranslation from "./compile.js"
5
5
  import setupGemini from "./gemini.js"
6
6
  import PO from "pofile"
7
- import { relative } from "node:path"
7
+ import { normalize, relative } from "node:path"
8
8
 
9
9
  export const defaultOptions = {
10
10
  sourceLocale: 'en',
@@ -35,7 +35,8 @@ async function loadPONoFail(filename) {
35
35
  if (!item.msgstr[0]) {
36
36
  untranslated++
37
37
  }
38
- translations[item.msgid] = item
38
+ const nTxt = new NestText(item.msgid, null, item.msgctxt)
39
+ translations[nTxt.toKey()] = item
39
40
  }
40
41
  }
41
42
  res({translations, total, obsolete, untranslated})
@@ -102,12 +103,12 @@ export default async function wuchale(options = defaultOptions) {
102
103
  // startup compile
103
104
  for (const loc of locales) {
104
105
  compiled[loc] = []
105
- for (const txt in translations[loc]) {
106
- const poItem = translations[loc][txt]
106
+ for (const key in translations[loc]) {
107
+ const poItem = translations[loc][key]
107
108
  if (forProduction) {
108
109
  poItem.references = []
109
110
  }
110
- const index = indexTracker.get(txt)
111
+ const index = indexTracker.get(key)
111
112
  compiled[loc][index] = compileTranslation(poItem.msgstr[0], compiled[options.sourceLocale][index])
112
113
  }
113
114
  await writeFile(compiledFname[loc], JSON.stringify(compiled[loc], null, 2))
@@ -128,38 +129,40 @@ export default async function wuchale(options = defaultOptions) {
128
129
  for (const loc of locales) {
129
130
  const newTxts = []
130
131
  for (const nTxt of txts) {
131
- const txt = nTxt.toString()
132
- let translated = translations[loc][txt]
132
+ let key = nTxt.toKey()
133
+ let translated = translations[loc][key]
133
134
  if (translated == null) {
134
135
  translated = new PO.Item()
135
- translated.msgid = txt
136
- translations[loc][txt] = translated
136
+ translated.msgid = nTxt.toString()
137
+ translations[loc][key] = translated
138
+ }
139
+ if (nTxt.context) {
140
+ translated.msgctxt = nTxt.context
137
141
  }
138
142
  if (!translated.references.includes(filename)) {
139
143
  translated.references.push(filename)
140
144
  }
141
145
  if (loc === options.sourceLocale) {
146
+ const txt = nTxt.toString()
142
147
  if (translated.msgstr[0] !== txt) {
143
148
  translated.msgstr = [txt]
144
- newTxts.push(txt)
149
+ newTxts.push(translated)
145
150
  }
146
151
  } else if (!translated.msgstr[0]) {
147
- newTxts.push(txt)
152
+ newTxts.push(translated)
148
153
  }
149
154
  }
150
155
  if (loc !== options.sourceLocale && newTxts.length) {
151
156
  const geminiT = setupGemini(options.sourceLocale, loc, options.geminiAPIKey)
152
157
  if (geminiT) {
153
- const gTrans = await geminiT(newTxts)
154
- for (const txt of newTxts) {
155
- translations[loc][txt].msgstr = [gTrans[txt]]
156
- }
158
+ console.info('Gemini translate', newTxts.length, 'items...')
159
+ await geminiT(newTxts) // will update because it's by reference
157
160
  }
158
161
  }
159
162
  for (const nTxt of txts) {
160
- const txt = nTxt.toString()
161
- const index = indexTracker.get(txt)
162
- compiled[loc][index] = compileTranslation(translations[loc][txt].msgstr[0], compiled[options.sourceLocale][index])
163
+ const key = nTxt.toKey()
164
+ const index = indexTracker.get(key)
165
+ compiled[loc][index] = compileTranslation(translations[loc][key].msgstr[0], compiled[options.sourceLocale][index])
163
166
  }
164
167
  for (const [i, c] of compiled[loc].entries()) {
165
168
  if (c == null) {
@@ -193,10 +196,10 @@ export default async function wuchale(options = defaultOptions) {
193
196
  * @param {string} id
194
197
  */
195
198
  handler: async function(code, id) {
196
- if (!id.startsWith(projectRoot)) {
199
+ if (!id.startsWith(projectRoot) || id.startsWith(normalize(projectRoot + '/node_modules'))) {
197
200
  return
198
201
  }
199
- const isModule = id.endsWith('.svelte.js')
202
+ const isModule = id.endsWith('.svelte.js') || id.endsWith('.svelte.ts')
200
203
  if (!id.endsWith('.svelte') && !isModule) {
201
204
  return
202
205
  }
@@ -223,8 +226,8 @@ export default async function wuchale(options = defaultOptions) {
223
226
  return
224
227
  }
225
228
  for (const loc of locales) {
226
- for (const txt in translations[loc]) {
227
- const poItem = translations[loc][txt]
229
+ for (const key in translations[loc]) {
230
+ const poItem = translations[loc][key]
228
231
  poItem.obsolete = poItem.references.length === 0
229
232
  }
230
233
  await savePO(translations[loc], translationsFname[loc])
@@ -8,7 +8,7 @@ const rtFunc = 'wuchaleTrans'
8
8
 
9
9
  /**
10
10
  * @typedef {"script" | "markup" | "attribute"} TxtScope
11
- * @typedef {(text: string, scope: TxtScope) => {extract: boolean; replace: string}} HeuristicFunc
11
+ * @typedef {(text: string, scope: TxtScope) => boolean} HeuristicFunc
12
12
  */
13
13
 
14
14
  /**
@@ -16,32 +16,29 @@ const rtFunc = 'wuchaleTrans'
16
16
  */
17
17
  export function defaultHeuristic(text, scope = 'markup') {
18
18
  if (scope === 'markup') {
19
- if (text.startsWith('-')) {
20
- return {extract: false, replace: text.slice(1).trim()}
21
- }
22
- return {extract: true, replace: text}
19
+ return true
23
20
  }
24
21
  // script and attribute
25
- if (text.startsWith('+')) {
26
- return {extract: true, replace: text.slice(1).trim()}
27
- }
28
22
  const range = 'AZ'
29
23
  const startCode = text.charCodeAt(0)
30
- if (startCode >= range.charCodeAt(0) && startCode <= range.charCodeAt(1)) {
31
- return {extract: true, replace: text}
32
- }
33
- return {extract: false, replace: text}
24
+ return startCode >= range.charCodeAt(0) && startCode <= range.charCodeAt(1)
34
25
  }
35
26
 
36
- class NestText extends String {
27
+ export class NestText extends String {
37
28
  /**
38
29
  * @param {string} txt
39
30
  * @param {TxtScope} scope
31
+ * @param {string | null} [context]
40
32
  */
41
- constructor(txt, scope) {
33
+ constructor(txt, scope, context) {
42
34
  super(txt)
43
35
  this.scope = scope
36
+ /** @type {string} */
37
+ this.context = context ?? null
44
38
  }
39
+
40
+ toKey = () => `${this.toString()}\n${this.context ?? ''}`.trim()
41
+
45
42
  }
46
43
 
47
44
  export class IndexTracker {
@@ -75,34 +72,32 @@ export default class Preprocess {
75
72
  * @param {HeuristicFunc} heuristic
76
73
  * @param {string} importFrom
77
74
  */
78
- constructor(index, heuristic, importFrom) {
75
+ constructor(index, heuristic = defaultHeuristic, importFrom) {
79
76
  this.index = index
80
77
  this.importFrom = importFrom
81
78
  this.heuristic = heuristic
82
79
  this.content = ''
83
80
  /** @type {MagicString} */
84
81
  this.mstr = null
82
+ /** @type {boolean | null} */
83
+ this.forceInclude = null
84
+ /** @type {string} */
85
+ this.context = null
85
86
  }
86
87
 
87
88
  /**
88
- * @param {number} start
89
- * @param {number} end
90
89
  * @param {string} text
91
90
  * @param {TxtScope} scope
92
91
  * @returns {Array<*> & {0: boolean, 1: NestText}}
93
92
  */
94
- modifyCheck = (start, end, text, scope) => {
93
+ checkHeuristic = (text, scope) => {
95
94
  text = text.replace(/\s+/g, ' ').trim()
96
95
  if (text === '') {
97
96
  // nothing to ask
98
97
  return [false, null]
99
98
  }
100
- let {extract, replace} = this.heuristic(text, scope)
101
- replace = replace.trim()
102
- if (!extract && text !== replace) {
103
- this.mstr.update(start, end, replace)
104
- }
105
- return [extract, new NestText(replace, scope)]
99
+ const extract = this.forceInclude || this.heuristic(text, scope)
100
+ return [extract, new NestText(text, scope)]
106
101
  }
107
102
 
108
103
  // visitComment = () => []
@@ -118,11 +113,12 @@ export default class Preprocess {
118
113
  return []
119
114
  }
120
115
  const { start, end } = node
121
- const [pass, txt] = this.modifyCheck(start, end, node.value, 'script')
116
+ const [pass, txt] = this.checkHeuristic(node.value, 'script')
122
117
  if (!pass) {
123
118
  return []
124
119
  }
125
- this.mstr.update(start, end, `${rtFunc}(${this.index.get(txt.toString())})`)
120
+ txt.context = this.context
121
+ this.mstr.update(start, end, `${rtFunc}(${this.index.get(txt.toKey())})`)
126
122
  return [txt]
127
123
  }
128
124
 
@@ -176,6 +172,17 @@ export default class Preprocess {
176
172
  return txts
177
173
  }
178
174
 
175
+ /**
176
+ * @param {import('estree').BinaryExpression} node
177
+ * @returns {NestText[]}
178
+ */
179
+ visitBinaryExpression = node => {
180
+ return [
181
+ ...this.visit(node.left),
182
+ ...this.visit(node.right),
183
+ ]
184
+ }
185
+
179
186
  /**
180
187
  * @param {import('estree').VariableDeclaration} node
181
188
  * @returns {NestText[]}
@@ -214,15 +221,15 @@ export default class Preprocess {
214
221
  const quasi0 = node.quasis[0]
215
222
  // @ts-ignore
216
223
  const {start: start0, end: end0} = quasi0
217
- const [pass, txt] = this.modifyCheck(start0, end0, quasi0.value.cooked, 'script')
224
+ const [pass, txt0] = this.checkHeuristic(quasi0.value.cooked, 'script')
218
225
  if (!pass) {
219
226
  return txts
220
227
  }
221
- let nTxt = txt.toString()
228
+ let txt = txt0.toString()
222
229
  for (const [i, expr] of node.expressions.entries()) {
223
230
  txts.push(...this.visit(expr))
224
231
  const quasi = node.quasis[i + 1]
225
- nTxt += `{${i}}${quasi.value.cooked}`
232
+ txt += `{${i}}${quasi.value.cooked}`
226
233
  // @ts-ignore
227
234
  const {start, end} = quasi
228
235
  this.mstr.remove(start - 1, end)
@@ -231,14 +238,15 @@ export default class Preprocess {
231
238
  }
232
239
  this.mstr.update(end, end + 2, ', ')
233
240
  }
234
- let repl = `${rtFunc}(${this.index.get(txt.toString())}`
241
+ const nTxt = new NestText(txt, txt0.scope, this.context)
242
+ let repl = `${rtFunc}(${this.index.get(nTxt.toKey())}`
235
243
  if (node.expressions.length) {
236
244
  repl += ', '
237
245
  }
238
246
  this.mstr.update(start0 - 1, end0 + 2, repl)
239
247
  // @ts-ignore
240
248
  this.mstr.update(node.end - 1, node.end, ')')
241
- txts.push(new NestText(nTxt, 'script'))
249
+ txts.push(nTxt)
242
250
  return txts
243
251
  }
244
252
 
@@ -290,8 +298,7 @@ export default class Preprocess {
290
298
  const textNodesToModify = {}
291
299
  for (const [i, child] of node.fragment.nodes.entries()) {
292
300
  if (child.type === 'Text') {
293
- const { start, end } = child
294
- const [pass, modify] = this.modifyCheck(start, end, child.data, 'markup')
301
+ const [pass, modify] = this.checkHeuristic(child.data, 'markup')
295
302
  if (pass) {
296
303
  hasTextChild = true
297
304
  textNodesToModify[i] = modify
@@ -374,7 +381,7 @@ export default class Preprocess {
374
381
  if (!txt) {
375
382
  return txts
376
383
  }
377
- const nTxt = new NestText(txt, 'markup')
384
+ const nTxt = new NestText(txt, 'markup', this.context)
378
385
  txts.push(nTxt)
379
386
  if (iTag > 0) {
380
387
  const snippets = []
@@ -386,7 +393,7 @@ export default class Preprocess {
386
393
  if (node.inCompoundText) {
387
394
  begin += `ctx={ctx}`
388
395
  } else {
389
- begin += `id={${this.index.get(txt)}}`
396
+ begin += `id={${this.index.get(nTxt.toKey())}}`
390
397
  }
391
398
  let end = ' />\n'
392
399
  if (iArg > 0) {
@@ -396,7 +403,7 @@ export default class Preprocess {
396
403
  this.mstr.appendLeft(lastChildEnd, begin)
397
404
  this.mstr.appendRight(lastChildEnd, end)
398
405
  } else if (!node.inCompoundText) {
399
- this.mstr.appendLeft(lastChildEnd, `{${rtFunc}(${this.index.get(txt)}, `)
406
+ this.mstr.appendLeft(lastChildEnd, `{${rtFunc}(${this.index.get(nTxt.toKey())}, `)
400
407
  this.mstr.appendRight(lastChildEnd, ')}')
401
408
  }
402
409
  return txts
@@ -409,12 +416,12 @@ export default class Preprocess {
409
416
  * @returns {NestText[]}
410
417
  */
411
418
  visitText = node => {
412
- const { start, end } = node
413
- const [pass, txt] = this.modifyCheck(start, end, node.data, 'markup')
419
+ const [pass, txt] = this.checkHeuristic(node.data, 'markup')
414
420
  if (!pass) {
415
421
  return []
416
422
  }
417
- this.mstr.update(node.start, node.end, `{${rtFunc}(${this.index.get(txt.toString())})}`)
423
+ txt.context = this.context
424
+ this.mstr.update(node.start, node.end, `{${rtFunc}(${this.index.get(txt.toKey())})}`)
418
425
  return [txt]
419
426
  }
420
427
 
@@ -448,12 +455,13 @@ export default class Preprocess {
448
455
  }
449
456
  // Text
450
457
  const {start, end} = value
451
- const [pass, txt] = this.modifyCheck(start, end, value.data, 'attribute')
458
+ const [pass, txt] = this.checkHeuristic(value.data, 'attribute')
452
459
  if (!pass) {
453
460
  continue
454
461
  }
462
+ txt.context = this.context
455
463
  txts.push(txt)
456
- this.mstr.update(value.start, value.end, `{${rtFunc}(${this.index.get(txt.toString())})}`)
464
+ this.mstr.update(value.start, value.end, `{${rtFunc}(${this.index.get(txt.toKey())})}`)
457
465
  if (!`'"`.includes(this.content[start - 1])) {
458
466
  continue
459
467
  }
@@ -586,16 +594,49 @@ export default class Preprocess {
586
594
  }
587
595
 
588
596
  /**
589
- * @param {import("svelte/compiler").AST.SvelteNode} node
597
+ * @param {string} data
598
+ */
599
+ processCommentDirectives = data => {
600
+ if (data === '@wc-ignore') {
601
+ this.forceInclude = false
602
+ }
603
+ if (data === '@wc-include') {
604
+ this.forceInclude = true
605
+ }
606
+ const ctxStart = '@wc-context:'
607
+ if (data.startsWith(ctxStart)) {
608
+ this.context = data.slice(ctxStart.length).trim()
609
+ }
610
+ }
611
+
612
+ /**
613
+ * @param {import("svelte/compiler").AST.SvelteNode & import('estree').BaseNode} node
590
614
  * @returns {NestText[]}
591
615
  */
592
616
  visit = node => {
593
- const methodName = `visit${node.type}`
594
- if (methodName in this) {
595
- return this[methodName](node)
617
+ if (node.type === 'Comment') {
618
+ this.processCommentDirectives(node.data.trim())
619
+ return []
620
+ }
621
+ // for estree
622
+ for (const comment of node.leadingComments ?? []) {
623
+ this.processCommentDirectives(comment.value.trim())
624
+ }
625
+ let txts = []
626
+ if (this.forceInclude !== false) {
627
+ const methodName = `visit${node.type}`
628
+ if (methodName in this) {
629
+ txts = this[methodName](node)
630
+ }
596
631
  }
597
- // console.log(node)
598
- return []
632
+ this.forceInclude = null
633
+ if (this.context != null) {
634
+ for (const txt of txts) {
635
+ txt.context = this.context
636
+ }
637
+ }
638
+ this.context = null
639
+ return txts
599
640
  }
600
641
 
601
642
  /**