@octanejs/i18next 0.1.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.
@@ -0,0 +1,216 @@
1
+ // Ported from react-i18next@17.0.9 (8b4a9ea): declarations produce octane descriptors.
2
+ import { createElement, positionalChildren } from 'octane';
3
+
4
+ import { TranslationParserError } from './TranslationParserError.js';
5
+ import { tokenize } from './tokenizer.js';
6
+ import { decodeHtmlEntities } from './htmlEntityDecoder.js';
7
+
8
+ /**
9
+ * Render a React element tree from a declaration node and its children
10
+ *
11
+ * @param {Object} declaration - The component declaration (type + props)
12
+ * @param {Array<React.ReactNode>} children - Array of child nodes (text, numbers, React elements)
13
+ * @param {Array<Object>} [childDeclarations] - Optional array of child declarations to use for nested rendering
14
+ * @returns {React.ReactElement} A React element
15
+ */
16
+ const renderDeclarationNode = (declaration, children, childDeclarations) => {
17
+ const { type, props = {} } = declaration;
18
+
19
+ // If props contain a children declaration AND we have childDeclarations to work with,
20
+ // we need to recursively render the content with those child declarations
21
+ if (props.children && Array.isArray(props.children) && childDeclarations) {
22
+ // The children array contains the parsed content from inside this tag
23
+ // We need to rebuild the translation string and re-parse it with child declarations
24
+ // For now, we'll use the children directly as they're already parsed
25
+ // This happens when renderTranslation is called recursively
26
+
27
+ // Remove children from props since we'll pass them as the third argument
28
+ // eslint-disable-next-line no-unused-vars
29
+ const { children: _childrenToRemove, ...propsWithoutChildren } = props;
30
+
31
+ return createElement(type, propsWithoutChildren, ...children);
32
+ }
33
+
34
+ // Standard rendering with children from translation
35
+ if (children.length === 0) {
36
+ return createElement(type, props);
37
+ }
38
+ if (children.length === 1) {
39
+ return createElement(type, props, children[0]);
40
+ }
41
+ return createElement(type, props, ...children);
42
+ };
43
+
44
+ /**
45
+ * Render translation string with declaration tree to create React elements
46
+ *
47
+ * This function parses an ICU format translation string and reconstructs
48
+ * a React element tree using the provided declaration tree. It replaces
49
+ * numbered tags (e.g., <0>, <1>) with the corresponding components from
50
+ * the declaration array and fills them with the translated text.
51
+ *
52
+ * @param {string} translation - ICU format string (e.g., "<0>Click here</0>")
53
+ * @param {Array<Object>} [declarations=[]] - Array of component declarations matching tag numbers
54
+ * @returns {Array<React.ReactNode>} Array of React nodes (elements and text)
55
+ *
56
+ * @example
57
+ * ```jsx
58
+ * const result = renderTranslation(
59
+ * "<0>bonjour</0> monde",
60
+ * [{ type: 'strong', props: { className: 'bold' } }]
61
+ * );
62
+ * // Returns: [<strong className="bold">bonjour</strong>, " monde"]
63
+ * ```
64
+ *
65
+ * @example
66
+ * ```jsx
67
+ * // With nested children in declaration
68
+ * const result = renderTranslation(
69
+ * "<0>Click <1>here</1></0>",
70
+ * [
71
+ * {
72
+ * type: 'div',
73
+ * props: {
74
+ * children: [{ type: 'span', props: {} }]
75
+ * }
76
+ * }
77
+ * ]
78
+ * );
79
+ * ```
80
+ */
81
+ export const renderTranslation = (translation, declarations = []) => {
82
+ if (!translation) {
83
+ return [];
84
+ }
85
+
86
+ const tokens = tokenize(translation);
87
+ const result = [];
88
+ const stack = [];
89
+
90
+ // Track tag numbers that should be treated as literal text (no declaration found)
91
+ const literalTagNumbers = new Set();
92
+
93
+ // Helper to get the current declarations array based on context
94
+ const getCurrentDeclarations = () => {
95
+ if (stack.length === 0) {
96
+ return declarations;
97
+ }
98
+
99
+ const parentFrame = stack[stack.length - 1];
100
+
101
+ // If the parent declaration has children declarations, use those
102
+ if (
103
+ parentFrame.declaration.props?.children &&
104
+ Array.isArray(parentFrame.declaration.props.children)
105
+ ) {
106
+ return parentFrame.declaration.props.children;
107
+ }
108
+
109
+ // Otherwise, use the parent's declarations array
110
+ return parentFrame.declarations;
111
+ };
112
+
113
+ tokens.forEach((token) => {
114
+ // eslint-disable-next-line default-case
115
+ switch (token.type) {
116
+ case 'Text':
117
+ {
118
+ const decoded = decodeHtmlEntities(token.value);
119
+ const targetArray = stack.length > 0 ? stack[stack.length - 1].children : result;
120
+
121
+ targetArray.push(decoded);
122
+ }
123
+
124
+ break;
125
+
126
+ case 'TagOpen':
127
+ {
128
+ const { tagNumber } = token;
129
+ const currentDeclarations = getCurrentDeclarations();
130
+ const declaration = currentDeclarations[tagNumber];
131
+
132
+ if (!declaration) {
133
+ // No declaration found - treat this tag as literal text
134
+ literalTagNumbers.add(tagNumber);
135
+
136
+ const literalText = `<${tagNumber}>`;
137
+ const targetArray = stack.length > 0 ? stack[stack.length - 1].children : result;
138
+
139
+ targetArray.push(literalText);
140
+
141
+ break;
142
+ }
143
+
144
+ stack.push({
145
+ tagNumber,
146
+ children: [],
147
+ position: token.position,
148
+ declaration,
149
+ declarations: currentDeclarations,
150
+ });
151
+ }
152
+
153
+ break;
154
+
155
+ case 'TagClose':
156
+ {
157
+ const { tagNumber } = token;
158
+
159
+ // If this tag was treated as literal, output the closing tag as literal text
160
+ if (literalTagNumbers.has(tagNumber)) {
161
+ const literalText = `</${tagNumber}>`;
162
+ const literalTargetArray = stack.length > 0 ? stack[stack.length - 1].children : result;
163
+
164
+ literalTargetArray.push(literalText);
165
+
166
+ literalTagNumbers.delete(tagNumber);
167
+
168
+ break;
169
+ }
170
+
171
+ if (stack.length === 0) {
172
+ throw new TranslationParserError(
173
+ `Unexpected closing tag </${tagNumber}> at position ${token.position}`,
174
+ token.position,
175
+ translation,
176
+ );
177
+ }
178
+
179
+ const frame = stack.pop();
180
+
181
+ if (frame.tagNumber !== tagNumber) {
182
+ throw new TranslationParserError(
183
+ `Mismatched tags: expected </${frame.tagNumber}> but got </${tagNumber}> at position ${token.position}`,
184
+ token.position,
185
+ translation,
186
+ );
187
+ }
188
+
189
+ // Render the element using the declaration and collected children
190
+ const element = renderDeclarationNode(
191
+ frame.declaration,
192
+ frame.children,
193
+ frame.declarations,
194
+ );
195
+
196
+ const elementTargetArray = stack.length > 0 ? stack[stack.length - 1].children : result;
197
+
198
+ elementTargetArray.push(element);
199
+ }
200
+
201
+ break;
202
+ }
203
+ });
204
+
205
+ if (stack.length > 0) {
206
+ const unclosed = stack[stack.length - 1];
207
+
208
+ throw new TranslationParserError(
209
+ `Unclosed tag <${unclosed.tagNumber}> at position ${unclosed.position}`,
210
+ unclosed.position,
211
+ translation,
212
+ );
213
+ }
214
+
215
+ return positionalChildren(result);
216
+ };
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Tokenize a translation string with numbered tags
3
+ * Note: Variables are already interpolated by the i18n system before we receive the string
4
+ *
5
+ * @param {string} translation - Translation string with numbered tags
6
+ * @returns {Array<Token>} Array of tokens
7
+ */
8
+ export const tokenize = (translation) => {
9
+ const tokens = [];
10
+
11
+ let position = 0;
12
+
13
+ let currentText = '';
14
+
15
+ const flushText = () => {
16
+ if (currentText) {
17
+ tokens.push({
18
+ type: 'Text',
19
+ value: currentText,
20
+ position: position - currentText.length,
21
+ });
22
+
23
+ currentText = '';
24
+ }
25
+ };
26
+
27
+ while (position < translation.length) {
28
+ const char = translation[position];
29
+
30
+ // Check for opening tag: <0>, <1>, etc.
31
+ if (char === '<') {
32
+ const tagMatch = translation.slice(position).match(/^<(\d+)>/);
33
+
34
+ if (tagMatch) {
35
+ flushText();
36
+
37
+ tokens.push({
38
+ type: 'TagOpen',
39
+ value: tagMatch[0],
40
+ position,
41
+ tagNumber: parseInt(tagMatch[1], 10),
42
+ });
43
+
44
+ position += tagMatch[0].length;
45
+ } else {
46
+ // Check for closing tag: </0>, </1>, etc.
47
+ const closeTagMatch = translation.slice(position).match(/^<\/(\d+)>/);
48
+
49
+ if (closeTagMatch) {
50
+ flushText();
51
+
52
+ tokens.push({
53
+ type: 'TagClose',
54
+ value: closeTagMatch[0],
55
+ position,
56
+ tagNumber: parseInt(closeTagMatch[1], 10),
57
+ });
58
+
59
+ position += closeTagMatch[0].length;
60
+ } else {
61
+ // Regular text (including any { } characters that aren't our tags)
62
+ currentText += char;
63
+
64
+ position += 1;
65
+ }
66
+ }
67
+ } else {
68
+ // Regular text (including any { } characters that aren't our tags)
69
+ currentText += char;
70
+
71
+ position += 1;
72
+ }
73
+ }
74
+
75
+ flushText();
76
+
77
+ return tokens;
78
+ };
@@ -0,0 +1,147 @@
1
+ // Ported from react-i18next@17.0.9 (8b4a9ea): dynamic nodes are octane descriptors.
2
+ import { warn, warnOnce, isString } from './utils.js';
3
+ import { getI18n } from './i18nInstance.js';
4
+ import { renderTranslation } from './IcuTransUtils/index.js';
5
+
6
+ /**
7
+ * IcuTrans component for rendering ICU MessageFormat translations (without React Context)
8
+ *
9
+ * This is the core implementation without React hooks or context dependencies,
10
+ * making it suitable for use in any environment. It uses a declaration tree
11
+ * approach where components are defined as type + props blueprints, fetches
12
+ * the translated string via i18next, and reconstructs the React element tree
13
+ * by replacing numbered tags (<0>, <1>) with actual components.
14
+ *
15
+ * Key features:
16
+ * - No React hooks or context (can be used anywhere)
17
+ * - ICU MessageFormat compatible
18
+ * - Supports nested component declarations
19
+ * - Automatic HTML entity decoding
20
+ * - Graceful error handling with fallbacks
21
+ * - Merges default interpolation variables
22
+ *
23
+ * Note: Users should typically use the IcuTrans export which provides automatic
24
+ * context support. This component is exposed for advanced use cases where direct
25
+ * i18n instance control is needed, or for use outside of React Context.
26
+ *
27
+ * @param {Object} props - Component props
28
+ * @param {string} props.i18nKey - The i18n key to look up the translation
29
+ * @param {string} props.defaultTranslation - The default translation in ICU format with numbered tags (e.g., "<0>Click here</0>")
30
+ * @param {Array<{type: string|React.ComponentType, props?: Object}>} props.content - Declaration tree describing React components and their props
31
+ * @param {string|string[]} [props.ns] - Optional namespace(s) for the translation. Falls back to t.ns, then i18n.options.defaultNS, then 'translation'
32
+ * @param {Object} [props.values={}] - Optional values for ICU variable interpolation (merged with i18n.options.interpolation.defaultVariables if present)
33
+ * @param {Object} [props.i18n] - i18next instance. If not provided, uses global instance from getI18n()
34
+ * @param {Function} [props.t] - Custom translation function. If not provided, uses i18n.t.bind(i18n)
35
+ * @returns {React.ReactElement} React fragment containing the rendered translation
36
+ *
37
+ * @example
38
+ * ```jsx
39
+ * // Direct usage with i18n instance
40
+ * <IcuTransWithoutContext
41
+ * i18nKey="welcome.message"
42
+ * defaultTranslation="Welcome <0>back</0>!"
43
+ * content={[
44
+ * { type: 'strong', props: { className: 'highlight' } }
45
+ * ]}
46
+ * i18n={i18nInstance}
47
+ * />
48
+ * ```
49
+ *
50
+ * @example
51
+ * ```jsx
52
+ * // With nested declarations for list rendering
53
+ * <IcuTransWithoutContext
54
+ * i18nKey="features.list"
55
+ * defaultTranslation="Features: <0><0>Fast</0><1>Reliable</1><2>Secure</2></0>"
56
+ * content={[
57
+ * {
58
+ * type: 'ul',
59
+ * props: {
60
+ * children: [
61
+ * { type: 'li', props: {} },
62
+ * { type: 'li', props: {} },
63
+ * { type: 'li', props: {} }
64
+ * ]
65
+ * }
66
+ * }
67
+ * ]}
68
+ * i18n={i18nInstance}
69
+ * />
70
+ * ```
71
+ *
72
+ * @example
73
+ * ```jsx
74
+ * // With values for ICU variable interpolation
75
+ * <IcuTransWithoutContext
76
+ * i18nKey="greeting"
77
+ * defaultTranslation="Hello <0>{name}</0>!"
78
+ * content={[{ type: 'strong', props: {} }]}
79
+ * values={{ name: 'Alice' }}
80
+ * i18n={i18nInstance}
81
+ * />
82
+ * ```
83
+ */
84
+ export function IcuTransWithoutContext({
85
+ i18nKey,
86
+ defaultTranslation,
87
+ content,
88
+ ns,
89
+ values = {},
90
+ i18n: i18nFromProps,
91
+ t: tFromProps,
92
+ }) {
93
+ const i18n = i18nFromProps || getI18n();
94
+
95
+ if (!i18n) {
96
+ warnOnce(
97
+ i18n,
98
+ 'NO_I18NEXT_INSTANCE',
99
+ `IcuTrans: You need to pass in an i18next instance using i18nextReactModule`,
100
+ { i18nKey },
101
+ );
102
+ return defaultTranslation;
103
+ }
104
+
105
+ const t = tFromProps || i18n.t?.bind(i18n) || ((k) => k);
106
+
107
+ // prepare having a namespace
108
+ let namespaces = ns || t.ns || i18n.options?.defaultNS;
109
+ namespaces = isString(namespaces) ? [namespaces] : namespaces || ['translation'];
110
+
111
+ // Merge default interpolation variables if they exist
112
+ let mergedValues = values;
113
+ if (i18n.options?.interpolation?.defaultVariables) {
114
+ mergedValues =
115
+ values && Object.keys(values).length > 0
116
+ ? { ...values, ...i18n.options.interpolation.defaultVariables }
117
+ : { ...i18n.options.interpolation.defaultVariables };
118
+ }
119
+
120
+ // Get the translation, falling back to defaultTranslation
121
+ const translation = t(i18nKey, {
122
+ defaultValue: defaultTranslation,
123
+ ...mergedValues,
124
+ ns: namespaces,
125
+ });
126
+
127
+ // Render the translation with the declaration tree
128
+ try {
129
+ const rendered = renderTranslation(translation, content);
130
+
131
+ // Octane components may return an array directly; Fragment is a
132
+ // compiler sentinel rather than a runtime component constructor.
133
+ return rendered;
134
+ } catch (error) {
135
+ // If rendering fails, warn and fall back to the translation string
136
+ warn(
137
+ i18n,
138
+ 'ICU_TRANS_RENDER_ERROR',
139
+ `IcuTrans component error for key "${i18nKey}": ${error.message}`,
140
+ { i18nKey, error },
141
+ );
142
+
143
+ return translation;
144
+ }
145
+ }
146
+
147
+ IcuTransWithoutContext.displayName = 'IcuTransWithoutContext';
package/src/Trans.js ADDED
@@ -0,0 +1,46 @@
1
+ // Ported from react-i18next@17.0.9 (8b4a9ea): React context -> octane context.
2
+ import { useContext } from 'octane';
3
+ import { nodesToString, Trans as TransWithoutContext } from './TransWithoutContext.js';
4
+ import { getI18n, I18nContext } from './context.js';
5
+
6
+ export { nodesToString };
7
+
8
+ export function Trans({
9
+ children,
10
+ count,
11
+ parent,
12
+ i18nKey,
13
+ context,
14
+ tOptions = {},
15
+ values,
16
+ defaults,
17
+ components,
18
+ ns,
19
+ i18n: i18nFromProps,
20
+ t: tFromProps,
21
+ shouldUnescape,
22
+ ...additionalProps
23
+ }) {
24
+ const { i18n: i18nFromContext, defaultNS: defaultNSFromContext } = useContext(I18nContext) || {};
25
+ const i18n = i18nFromProps || i18nFromContext || getI18n();
26
+
27
+ const t = tFromProps || i18n?.t.bind(i18n);
28
+
29
+ return TransWithoutContext({
30
+ children,
31
+ count,
32
+ parent,
33
+ i18nKey,
34
+ context,
35
+ tOptions,
36
+ values,
37
+ defaults,
38
+ components,
39
+ // prepare having a namespace
40
+ ns: ns || t?.ns || defaultNSFromContext || i18n?.options?.defaultNS,
41
+ i18n,
42
+ t: tFromProps,
43
+ shouldUnescape,
44
+ ...additionalProps,
45
+ });
46
+ }
@@ -0,0 +1,160 @@
1
+ import type {
2
+ i18n,
3
+ ReactOptions,
4
+ ApplyTarget,
5
+ ConstrainTarget,
6
+ GetSource,
7
+ InterpolationMap,
8
+ ParseKeys,
9
+ Namespace,
10
+ SelectorFn,
11
+ SelectorKey,
12
+ TFunctionReturn,
13
+ TypeOptions,
14
+ TOptions,
15
+ TFunction,
16
+ } from 'i18next';
17
+ import type { ComponentBody, ElementDescriptor } from 'octane';
18
+
19
+ type _DefaultNamespace = TypeOptions['defaultNS'];
20
+ type _EnableSelector = TypeOptions['enableSelector'];
21
+ type _KeySeparator = TypeOptions['keySeparator'];
22
+ type _AppendKeyPrefix<Key, KPrefix> = KPrefix extends string
23
+ ? `${KPrefix}${_KeySeparator}${Key & string}`
24
+ : Key;
25
+
26
+ type TransChild = unknown;
27
+ type $NoInfer<T> = [T][T extends T ? 0 : never];
28
+
29
+ export type TransProps<
30
+ Key extends ParseKeys<Ns, TOpt, KPrefix>,
31
+ Ns extends Namespace = _DefaultNamespace,
32
+ KPrefix = undefined,
33
+ TContext extends string | undefined = undefined,
34
+ TOpt extends TOptions & { context?: TContext } = { context: TContext },
35
+ Ret = TFunctionReturn<Ns, _AppendKeyPrefix<Key, KPrefix>, TOpt>,
36
+ E = Record<string, unknown>,
37
+ > = E & {
38
+ children?: TransChild | readonly TransChild[];
39
+ components?: readonly ElementDescriptor[] | { readonly [tagName: string]: ElementDescriptor };
40
+ count?: number;
41
+ context?: TContext;
42
+ defaults?: string;
43
+ i18n?: i18n;
44
+ i18nKey?: Key | Key[];
45
+ // allow a single namespace from an array-typed `t` (e.g. useTranslation(['ns'])); TS7 intersects
46
+ // inference candidates from `t` and `ns`, so a bare `Ns` here rejects ns="ns" when t is passed
47
+ ns?: Ns | (Ns extends readonly (infer S extends string)[] ? S : never);
48
+ parent?: string | ComponentBody<any> | null;
49
+ tOptions?: TOpt;
50
+ values?: InterpolationMap<Ret>;
51
+ shouldUnescape?: boolean;
52
+ t?: TFunction<Ns, KPrefix>;
53
+ };
54
+
55
+ export interface TransLegacy {
56
+ <
57
+ const Key extends ParseKeys<Ns, TOpt, KPrefix>,
58
+ Ns extends Namespace = _DefaultNamespace,
59
+ KPrefix = undefined,
60
+ TContext extends string | undefined = undefined,
61
+ TOpt extends TOptions & { context?: TContext } = { context: TContext },
62
+ Ret extends TFunctionReturn<Ns, _AppendKeyPrefix<Key, KPrefix>, TOpt> = TFunctionReturn<
63
+ Ns,
64
+ _AppendKeyPrefix<Key, KPrefix>,
65
+ TOpt
66
+ >,
67
+ E = Record<string, unknown>,
68
+ >(
69
+ props: TransProps<Key, Ns, KPrefix, TContext, TOpt, Ret, E>,
70
+ ): unknown;
71
+ }
72
+
73
+ export interface TransSelectorProps<
74
+ Key,
75
+ Ns extends Namespace = _DefaultNamespace,
76
+ KPrefix = undefined,
77
+ TContext extends string | undefined = undefined,
78
+ TOpt extends TOptions & { context?: TContext } = { context: TContext },
79
+ > {
80
+ children?: TransChild | readonly TransChild[];
81
+ components?: readonly ElementDescriptor[] | { readonly [tagName: string]: ElementDescriptor };
82
+ count?: number;
83
+ context?: TContext;
84
+ defaults?: string | Key;
85
+ i18n?: i18n;
86
+ i18nKey?: Key | readonly Key[];
87
+ // see TransProps.ns: keep single-namespace values assignable when `t` fixes Ns to an array
88
+ ns?: Ns | (Ns extends readonly (infer S extends string)[] ? S : never);
89
+ parent?: string | ComponentBody<any> | null;
90
+ tOptions?: TOpt;
91
+ values?: Key extends (...args: any[]) => infer R ? InterpolationMap<R> : {};
92
+ shouldUnescape?: boolean;
93
+ t?: TFunction<Ns, KPrefix>;
94
+ }
95
+
96
+ export interface TransSelector {
97
+ <
98
+ Target extends ConstrainTarget<TOpt>,
99
+ Key extends
100
+ | SelectorFn<GetSource<$NoInfer<Ns>, KPrefix>, ApplyTarget<Target, TOpt>, TOpt>
101
+ | SelectorKey,
102
+ const Ns extends Namespace = _DefaultNamespace,
103
+ KPrefix = undefined,
104
+ TContext extends string | undefined = undefined,
105
+ TOpt extends TOptions & { context?: TContext } = { context: TContext },
106
+ E = Record<string, unknown>,
107
+ >(
108
+ props: TransSelectorProps<Key, Ns, KPrefix, TContext, TOpt> & E,
109
+ ): unknown;
110
+ }
111
+
112
+ export const Trans: _EnableSelector extends true | 'optimize' | 'strict'
113
+ ? TransSelector
114
+ : TransLegacy;
115
+
116
+ export function nodesToString(
117
+ children: unknown,
118
+ i18nOptions?: ReactOptions,
119
+ i18n?: i18n,
120
+ i18nKey?: string,
121
+ ): string;
122
+
123
+ export type ErrorCode =
124
+ | 'NO_I18NEXT_INSTANCE'
125
+ | 'NO_LANGUAGES'
126
+ | 'DEPRECATED_OPTION'
127
+ | 'TRANS_NULL_VALUE'
128
+ | 'TRANS_INVALID_OBJ'
129
+ | 'TRANS_INVALID_VAR'
130
+ | 'TRANS_INVALID_COMPONENTS'
131
+ | 'USE_T_BEFORE_READY'
132
+ | 'OCTANE_TRANS_BLOCK_CHILDREN'
133
+ | 'ICU_TRANS_RENDER_ERROR';
134
+
135
+ export type ErrorMeta = {
136
+ code: ErrorCode;
137
+ i18nKey?: string;
138
+ [x: string]: any;
139
+ };
140
+
141
+ /**
142
+ * Use to type the logger arguments
143
+ * @example
144
+ * ```
145
+ * import type { ErrorArgs } from 'react-i18next';
146
+ *
147
+ * const logger = {
148
+ * // ....
149
+ * warn: function (...args: ErrorArgs) {
150
+ * if (args[1]?.code === 'TRANS_INVALID_OBJ') {
151
+ * const [msg, { i18nKey, ...rest }] = args;
152
+ * return log(i18nKey, msg, rest);
153
+ * }
154
+ * log(...args);
155
+ * }
156
+ * }
157
+ * i18n.use(logger).use(i18nReactPlugin).init({...});
158
+ * ```
159
+ */
160
+ export type ErrorArgs = readonly [string, ErrorMeta | undefined, ...any[]];