i18next-cli 1.24.13 → 1.24.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/cli.js +1 -1
- package/dist/esm/cli.js +1 -1
- package/package.json +6 -6
- package/types/cli.d.ts +3 -1
- package/types/cli.d.ts.map +1 -1
- package/CHANGELOG.md +0 -599
- package/src/cli.ts +0 -283
- package/src/config.ts +0 -215
- package/src/extractor/core/ast-visitors.ts +0 -259
- package/src/extractor/core/extractor.ts +0 -250
- package/src/extractor/core/key-finder.ts +0 -142
- package/src/extractor/core/translation-manager.ts +0 -750
- package/src/extractor/index.ts +0 -7
- package/src/extractor/parsers/ast-utils.ts +0 -87
- package/src/extractor/parsers/call-expression-handler.ts +0 -793
- package/src/extractor/parsers/comment-parser.ts +0 -424
- package/src/extractor/parsers/expression-resolver.ts +0 -391
- package/src/extractor/parsers/jsx-handler.ts +0 -488
- package/src/extractor/parsers/jsx-parser.ts +0 -1463
- package/src/extractor/parsers/scope-manager.ts +0 -445
- package/src/extractor/plugin-manager.ts +0 -116
- package/src/extractor.ts +0 -15
- package/src/heuristic-config.ts +0 -92
- package/src/index.ts +0 -22
- package/src/init.ts +0 -175
- package/src/linter.ts +0 -345
- package/src/locize.ts +0 -263
- package/src/migrator.ts +0 -208
- package/src/rename-key.ts +0 -398
- package/src/status.ts +0 -380
- package/src/syncer.ts +0 -133
- package/src/types-generator.ts +0 -139
- package/src/types.ts +0 -577
- package/src/utils/default-value.ts +0 -45
- package/src/utils/file-utils.ts +0 -167
- package/src/utils/funnel-msg-tracker.ts +0 -84
- package/src/utils/logger.ts +0 -36
- package/src/utils/nested-object.ts +0 -135
- package/src/utils/validation.ts +0 -72
|
@@ -1,391 +0,0 @@
|
|
|
1
|
-
import type { Expression, TsType, TemplateLiteral, TsTemplateLiteralType } from '@swc/core'
|
|
2
|
-
import type { ASTVisitorHooks } from '../../types'
|
|
3
|
-
|
|
4
|
-
export class ExpressionResolver {
|
|
5
|
-
private hooks: ASTVisitorHooks
|
|
6
|
-
// Per-file symbol table for statically analyzable variables.
|
|
7
|
-
// Maps variableName -> either:
|
|
8
|
-
// - string[] (possible string values)
|
|
9
|
-
// - Record<string, string> (object of static string properties)
|
|
10
|
-
private variableTable: Map<string, string[] | Record<string, string>> = new Map()
|
|
11
|
-
|
|
12
|
-
// Shared (cross-file) table for enums / exported object maps that should persist
|
|
13
|
-
private sharedEnumTable: Map<string, Record<string, string>> = new Map()
|
|
14
|
-
|
|
15
|
-
constructor (hooks: ASTVisitorHooks) {
|
|
16
|
-
this.hooks = hooks
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Clear per-file captured variables. Enums / shared maps are kept.
|
|
21
|
-
*/
|
|
22
|
-
public resetFileSymbols (): void {
|
|
23
|
-
this.variableTable.clear()
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Capture a VariableDeclarator node to record simple statically analyzable
|
|
28
|
-
* initializers (string literals, object expressions of string literals,
|
|
29
|
-
* template literals and simple concatenations).
|
|
30
|
-
*
|
|
31
|
-
* This is called during AST traversal before deeper walking so later
|
|
32
|
-
* identifier/member-expression usage can be resolved.
|
|
33
|
-
*
|
|
34
|
-
* @param node - VariableDeclarator-like node (has .id and .init)
|
|
35
|
-
*/
|
|
36
|
-
captureVariableDeclarator (node: any): void {
|
|
37
|
-
try {
|
|
38
|
-
if (!node || !node.id || !node.init) return
|
|
39
|
-
// only handle simple identifier bindings like `const x = ...`
|
|
40
|
-
if (node.id.type !== 'Identifier') return
|
|
41
|
-
const name = node.id.value
|
|
42
|
-
const init = node.init
|
|
43
|
-
|
|
44
|
-
// ObjectExpression -> map of string props
|
|
45
|
-
if (init.type === 'ObjectExpression' && Array.isArray(init.properties)) {
|
|
46
|
-
const map: Record<string, string> = {}
|
|
47
|
-
for (const p of init.properties as any[]) {
|
|
48
|
-
if (!p || p.type !== 'KeyValueProperty') continue
|
|
49
|
-
const keyNode = p.key
|
|
50
|
-
const keyName = keyNode?.type === 'Identifier' ? keyNode.value : keyNode?.type === 'StringLiteral' ? keyNode.value : undefined
|
|
51
|
-
if (!keyName) continue
|
|
52
|
-
const valExpr = p.value
|
|
53
|
-
const vals = this.resolvePossibleStringValuesFromExpression(valExpr)
|
|
54
|
-
// Only capture properties that we can statically resolve to a single string.
|
|
55
|
-
if (vals.length === 1) {
|
|
56
|
-
map[keyName] = vals[0]
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
// If at least one property was resolvable, record the partial map.
|
|
60
|
-
if (Object.keys(map).length > 0) {
|
|
61
|
-
this.variableTable.set(name, map)
|
|
62
|
-
return
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// For other initializers, try to resolve to one-or-more strings
|
|
67
|
-
const vals = this.resolvePossibleStringValuesFromExpression(init)
|
|
68
|
-
if (vals.length > 0) {
|
|
69
|
-
this.variableTable.set(name, vals)
|
|
70
|
-
}
|
|
71
|
-
} catch {
|
|
72
|
-
// be silent - conservative only
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* Capture a TypeScript enum declaration so members can be resolved later.
|
|
78
|
-
* Accepts SWC node shapes like `TsEnumDeclaration` / `TSEnumDeclaration`.
|
|
79
|
-
*
|
|
80
|
-
* Enums are stored in the shared table so they are available across files.
|
|
81
|
-
*/
|
|
82
|
-
captureEnumDeclaration (node: any): void {
|
|
83
|
-
try {
|
|
84
|
-
if (!node || !node.id || !Array.isArray(node.members)) return
|
|
85
|
-
const name = node.id.type === 'Identifier' ? node.id.value : undefined
|
|
86
|
-
if (!name) return
|
|
87
|
-
const map: Record<string, string> = {}
|
|
88
|
-
for (const m of node.members) {
|
|
89
|
-
if (!m || !m.id) continue
|
|
90
|
-
const keyNode = m.id
|
|
91
|
-
const memberName = keyNode.type === 'Identifier' ? keyNode.value : keyNode.type === 'StringLiteral' ? keyNode.value : undefined
|
|
92
|
-
if (!memberName) continue
|
|
93
|
-
const init = (m as any).init ?? (m as any).initializer
|
|
94
|
-
if (init && init.type === 'StringLiteral') {
|
|
95
|
-
map[memberName] = init.value
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
if (Object.keys(map).length > 0) {
|
|
99
|
-
this.sharedEnumTable.set(name, map)
|
|
100
|
-
}
|
|
101
|
-
} catch {
|
|
102
|
-
// noop
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* Resolves an expression to one or more possible context string values that can be
|
|
108
|
-
* determined statically from the AST. This is a wrapper around the plugin hook
|
|
109
|
-
* `extractContextFromExpression` and {@link resolvePossibleStringValuesFromExpression}.
|
|
110
|
-
*
|
|
111
|
-
* @param expression - The SWC AST expression node to resolve
|
|
112
|
-
* @returns An array of possible context string values that the expression may produce.
|
|
113
|
-
*/
|
|
114
|
-
resolvePossibleContextStringValues (expression: Expression): string[] {
|
|
115
|
-
const strings = this.hooks.resolvePossibleContextStringValues?.(expression) ?? []
|
|
116
|
-
return [...strings, ...this.resolvePossibleStringValuesFromExpression(expression)]
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
/**
|
|
120
|
-
* Resolves an expression to one or more possible key string values that can be
|
|
121
|
-
* determined statically from the AST. This is a wrapper around the plugin hook
|
|
122
|
-
* `extractKeysFromExpression` and {@link resolvePossibleStringValuesFromExpression}.
|
|
123
|
-
*
|
|
124
|
-
* @param expression - The SWC AST expression node to resolve
|
|
125
|
-
* @returns An array of possible key string values that the expression may produce.
|
|
126
|
-
*/
|
|
127
|
-
resolvePossibleKeyStringValues (expression: Expression): string[] {
|
|
128
|
-
const strings = this.hooks.resolvePossibleKeyStringValues?.(expression) ?? []
|
|
129
|
-
return [...strings, ...this.resolvePossibleStringValuesFromExpression(expression)]
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
/**
|
|
133
|
-
* Resolves an expression to one or more possible string values that can be
|
|
134
|
-
* determined statically from the AST.
|
|
135
|
-
*
|
|
136
|
-
* Supports:
|
|
137
|
-
* - StringLiteral -> single value (filtered to exclude empty strings for context)
|
|
138
|
-
* - NumericLiteral -> single value
|
|
139
|
-
* - BooleanLiteral -> single value
|
|
140
|
-
* - ConditionalExpression (ternary) -> union of consequent and alternate resolved values
|
|
141
|
-
* - TemplateLiteral -> union of all possible string values
|
|
142
|
-
* - The identifier `undefined` -> empty array
|
|
143
|
-
*
|
|
144
|
-
* For any other expression types (identifiers, function calls, member expressions,
|
|
145
|
-
* etc.) the value cannot be determined statically and an empty array is returned.
|
|
146
|
-
*
|
|
147
|
-
* @param expression - The SWC AST expression node to resolve
|
|
148
|
-
* @param returnEmptyStrings - Whether to include empty strings in the result
|
|
149
|
-
* @returns An array of possible string values that the expression may produce.
|
|
150
|
-
*/
|
|
151
|
-
private resolvePossibleStringValuesFromExpression (expression: Expression, returnEmptyStrings = false): string[] {
|
|
152
|
-
// Support selector-style arrow functions used by the selector API:
|
|
153
|
-
// e.g. ($) => $.path.to.key -> 'path.to.key'
|
|
154
|
-
if (expression.type === 'ArrowFunctionExpression') {
|
|
155
|
-
try {
|
|
156
|
-
let body: any = expression.body
|
|
157
|
-
// Handle block body with return statement
|
|
158
|
-
if (body.type === 'BlockStatement') {
|
|
159
|
-
const returnStmt = body.stmts.find((s: any) => s.type === 'ReturnStatement')
|
|
160
|
-
if (returnStmt?.type === 'ReturnStatement' && returnStmt.argument) {
|
|
161
|
-
body = returnStmt.argument
|
|
162
|
-
} else {
|
|
163
|
-
return []
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
let current: any = body
|
|
168
|
-
const parts: string[] = []
|
|
169
|
-
|
|
170
|
-
while (current && current.type === 'MemberExpression') {
|
|
171
|
-
const prop = current.property
|
|
172
|
-
if (prop.type === 'Identifier') {
|
|
173
|
-
parts.unshift(prop.value)
|
|
174
|
-
} else if (prop.type === 'Computed' && prop.expression && prop.expression.type === 'StringLiteral') {
|
|
175
|
-
parts.unshift(prop.expression.value)
|
|
176
|
-
} else {
|
|
177
|
-
return []
|
|
178
|
-
}
|
|
179
|
-
current = current.object
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
if (parts.length > 0) {
|
|
183
|
-
return [parts.join('.')]
|
|
184
|
-
}
|
|
185
|
-
} catch {
|
|
186
|
-
return []
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
if (expression.type === 'StringLiteral') {
|
|
191
|
-
// Filter out empty strings as they should be treated as "no context" like i18next does
|
|
192
|
-
return expression.value || returnEmptyStrings ? [expression.value] : []
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
if (expression.type === 'ConditionalExpression') { // This is a ternary operator
|
|
196
|
-
const consequentValues = this.resolvePossibleStringValuesFromExpression(expression.consequent, returnEmptyStrings)
|
|
197
|
-
const alternateValues = this.resolvePossibleStringValuesFromExpression(expression.alternate, returnEmptyStrings)
|
|
198
|
-
return [...consequentValues, ...alternateValues]
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
if (expression.type === 'Identifier' && expression.value === 'undefined') {
|
|
202
|
-
return [] // Handle the `undefined` case
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
if (expression.type === 'TemplateLiteral') {
|
|
206
|
-
return this.resolvePossibleStringValuesFromTemplateString(expression)
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
// MemberExpression: try to resolve object identifier to an object map in the symbol table
|
|
210
|
-
if (expression.type === 'MemberExpression') {
|
|
211
|
-
try {
|
|
212
|
-
const obj = expression.object
|
|
213
|
-
const prop = expression.property
|
|
214
|
-
// only handle simple identifier base + simple property (Identifier or computed StringLiteral)
|
|
215
|
-
if (obj.type === 'Identifier') {
|
|
216
|
-
const baseVar = this.variableTable.get(obj.value)
|
|
217
|
-
const baseShared = this.sharedEnumTable.get(obj.value)
|
|
218
|
-
const base = baseVar ?? baseShared
|
|
219
|
-
if (base && typeof base !== 'string' && !Array.isArray(base)) {
|
|
220
|
-
let propName: string | undefined
|
|
221
|
-
if (prop.type === 'Identifier') propName = prop.value
|
|
222
|
-
else if (prop.type === 'Computed' && prop.expression?.type === 'StringLiteral') propName = prop.expression.value
|
|
223
|
-
if (propName && base[propName] !== undefined) {
|
|
224
|
-
return [base[propName]]
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
} catch {}
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
// Binary concatenation support (e.g., a + '_' + b)
|
|
232
|
-
// SWC binary expr can be represented as `BinExpr` with left/right; be permissive:
|
|
233
|
-
if ((expression as any).left && (expression as any).right) {
|
|
234
|
-
try {
|
|
235
|
-
const exprAny = expression as any
|
|
236
|
-
const leftNode = exprAny.left
|
|
237
|
-
const rightNode = exprAny.right
|
|
238
|
-
|
|
239
|
-
// Detect explicit binary concatenation (plus) nodes and only then produce concatenated combos.
|
|
240
|
-
const isBinaryConcat =
|
|
241
|
-
// SWC older shape: BinExpr with op === '+'
|
|
242
|
-
(exprAny.type === 'BinExpr' && exprAny.op === '+') ||
|
|
243
|
-
// Standard AST: BinaryExpression with operator === '+'
|
|
244
|
-
(exprAny.type === 'BinaryExpression' && exprAny.operator === '+') ||
|
|
245
|
-
// Fallbacks
|
|
246
|
-
exprAny.operator === '+' || exprAny.op === '+'
|
|
247
|
-
|
|
248
|
-
if (isBinaryConcat) {
|
|
249
|
-
const leftVals = this.resolvePossibleStringValuesFromExpression(leftNode, returnEmptyStrings)
|
|
250
|
-
const rightVals = this.resolvePossibleStringValuesFromExpression(rightNode, returnEmptyStrings)
|
|
251
|
-
if (leftVals.length > 0 && rightVals.length > 0) {
|
|
252
|
-
const combos: string[] = []
|
|
253
|
-
for (const L of leftVals) {
|
|
254
|
-
for (const R of rightVals) {
|
|
255
|
-
combos.push(`${L}${R}`)
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
return combos
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
// Handle logical nullish coalescing (a ?? b): result is either left (when not null/undefined) OR right.
|
|
263
|
-
// Represent this conservatively as the union of possible left and right values.
|
|
264
|
-
const isNullishCoalesce =
|
|
265
|
-
// SWC may emit as BinaryExpression with operator '??'
|
|
266
|
-
(exprAny.type === 'BinaryExpression' && exprAny.operator === '??') ||
|
|
267
|
-
(exprAny.type === 'LogicalExpression' && exprAny.operator === '??') ||
|
|
268
|
-
exprAny.operator === '??' || exprAny.op === '??'
|
|
269
|
-
|
|
270
|
-
if (isNullishCoalesce) {
|
|
271
|
-
const leftVals = this.resolvePossibleStringValuesFromExpression(leftNode, returnEmptyStrings)
|
|
272
|
-
const rightVals = this.resolvePossibleStringValuesFromExpression(rightNode, returnEmptyStrings)
|
|
273
|
-
if (leftVals.length > 0 || rightVals.length > 0) {
|
|
274
|
-
return Array.from(new Set([...leftVals, ...rightVals]))
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
} catch {}
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
if (expression.type === 'NumericLiteral' || expression.type === 'BooleanLiteral') {
|
|
281
|
-
return [`${expression.value}`] // Handle literals like 5 or true
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
// Support building translation keys for
|
|
285
|
-
// `variable satisfies 'coaching' | 'therapy'`
|
|
286
|
-
if (expression.type === 'TsSatisfiesExpression' || expression.type === 'TsAsExpression') {
|
|
287
|
-
const annotation = expression.typeAnnotation
|
|
288
|
-
return this.resolvePossibleStringValuesFromType(annotation, returnEmptyStrings)
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
// Identifier resolution via captured per-file variable table only
|
|
292
|
-
if (expression.type === 'Identifier') {
|
|
293
|
-
const v = this.variableTable.get(expression.value)
|
|
294
|
-
if (!v) return []
|
|
295
|
-
if (Array.isArray(v)) return v
|
|
296
|
-
// object map - cannot be used directly as key, so return empty
|
|
297
|
-
return []
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
// We can't statically determine the value of other expressions (e.g., variables, function calls)
|
|
301
|
-
return []
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
private resolvePossibleStringValuesFromType (type: TsType, returnEmptyStrings = false): string[] {
|
|
305
|
-
if (type.type === 'TsUnionType') {
|
|
306
|
-
return type.types.flatMap((t) => this.resolvePossibleStringValuesFromType(t, returnEmptyStrings))
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
if (type.type === 'TsLiteralType') {
|
|
310
|
-
if (type.literal.type === 'StringLiteral') {
|
|
311
|
-
// Filter out empty strings as they should be treated as "no context" like i18next does
|
|
312
|
-
return type.literal.value || returnEmptyStrings ? [type.literal.value] : []
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
if (type.literal.type === 'TemplateLiteral') {
|
|
316
|
-
return this.resolvePossibleStringValuesFromTemplateLiteralType(type.literal)
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
if (type.literal.type === 'NumericLiteral' || type.literal.type === 'BooleanLiteral') {
|
|
320
|
-
return [`${type.literal.value}`] // Handle literals like 5 or true
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
// We can't statically determine the value of other expressions (e.g., variables, function calls)
|
|
325
|
-
return []
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
/**
|
|
329
|
-
* Resolves a template literal string to one or more possible strings that can be
|
|
330
|
-
* determined statically from the AST.
|
|
331
|
-
*
|
|
332
|
-
* @param templateString - The SWC AST template literal string to resolve
|
|
333
|
-
* @returns An array of possible string values that the template may produce.
|
|
334
|
-
*/
|
|
335
|
-
private resolvePossibleStringValuesFromTemplateString (templateString: TemplateLiteral): string[] {
|
|
336
|
-
// If there are no expressions, we can just return the cooked value
|
|
337
|
-
if (templateString.quasis.length === 1 && templateString.expressions.length === 0) {
|
|
338
|
-
// Ex. `translation.key.no.substitution`
|
|
339
|
-
return [templateString.quasis[0].cooked || '']
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
// Ex. `translation.key.with.expression.${x ? 'title' : 'description'}`
|
|
343
|
-
const [firstQuasis, ...tails] = templateString.quasis
|
|
344
|
-
|
|
345
|
-
const stringValues = templateString.expressions.reduce(
|
|
346
|
-
(heads, expression, i) => {
|
|
347
|
-
return heads.flatMap((head) => {
|
|
348
|
-
const tail = tails[i]?.cooked ?? ''
|
|
349
|
-
return this.resolvePossibleStringValuesFromExpression(expression, true).map(
|
|
350
|
-
(expressionValue) => `${head}${expressionValue}${tail}`
|
|
351
|
-
)
|
|
352
|
-
})
|
|
353
|
-
},
|
|
354
|
-
[firstQuasis.cooked ?? '']
|
|
355
|
-
)
|
|
356
|
-
|
|
357
|
-
return stringValues
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
/**
|
|
361
|
-
* Resolves a template literal type to one or more possible strings that can be
|
|
362
|
-
* determined statically from the AST.
|
|
363
|
-
*
|
|
364
|
-
* @param templateLiteralType - The SWC AST template literal type to resolve
|
|
365
|
-
* @returns An array of possible string values that the template may produce.
|
|
366
|
-
*/
|
|
367
|
-
private resolvePossibleStringValuesFromTemplateLiteralType (templateLiteralType: TsTemplateLiteralType): string[] {
|
|
368
|
-
// If there are no types, we can just return the cooked value
|
|
369
|
-
if (templateLiteralType.quasis.length === 1 && templateLiteralType.types.length === 0) {
|
|
370
|
-
// Ex. `translation.key.no.substitution`
|
|
371
|
-
return [templateLiteralType.quasis[0].cooked || '']
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
// Ex. `translation.key.with.expression.${'title' | 'description'}`
|
|
375
|
-
const [firstQuasis, ...tails] = templateLiteralType.quasis
|
|
376
|
-
|
|
377
|
-
const stringValues = templateLiteralType.types.reduce(
|
|
378
|
-
(heads, type, i) => {
|
|
379
|
-
return heads.flatMap((head) => {
|
|
380
|
-
const tail = tails[i]?.cooked ?? ''
|
|
381
|
-
return this.resolvePossibleStringValuesFromType(type, true).map(
|
|
382
|
-
(expressionValue) => `${head}${expressionValue}${tail}`
|
|
383
|
-
)
|
|
384
|
-
})
|
|
385
|
-
},
|
|
386
|
-
[firstQuasis.cooked ?? '']
|
|
387
|
-
)
|
|
388
|
-
|
|
389
|
-
return stringValues
|
|
390
|
-
}
|
|
391
|
-
}
|