From 1c248a2fef8ca4917d459132f1804afe1d86688c Mon Sep 17 00:00:00 2001 From: Timofei Iatsenko Date: Fri, 18 Oct 2024 14:27:08 +0200 Subject: [PATCH] feat(no-unlocalized-strings): add regex patterns support (#70) * feat(no-unlocalized-strings): add regex patterns for ignoreAttribute, strictAttribute, ignoreProperty * feat(no-unlocalized-strings): improve the readme --- docs/rules/no-unlocalized-strings.md | 181 ++++++++++---- src/helpers.ts | 28 ++- src/rules/no-single-variables-to-translate.ts | 3 +- src/rules/no-unlocalized-strings.ts | 226 ++++++++++-------- .../src/rules/no-unlocalized-strings.test.ts | 57 +++-- 5 files changed, 332 insertions(+), 163 deletions(-) diff --git a/docs/rules/no-unlocalized-strings.md b/docs/rules/no-unlocalized-strings.md index 85fa7e8..e9f520f 100644 --- a/docs/rules/no-unlocalized-strings.md +++ b/docs/rules/no-unlocalized-strings.md @@ -1,44 +1,42 @@ # no-unlocalized-strings -Check that code doesn't contain strings/templates/jsxText what should be wrapped into `` or `i18n` +Ensures that all string literals, templates, and JSX text are wrapped using ``, `t`, or `msg` for localization. > [!IMPORTANT] -> This rule might use type information. You can enable it with `{useTsTypes: true}` +> This rule may require TypeScript type information. Enable this feature by setting `{ useTsTypes: true }`. ## Options -### useTsTypes +### `useTsTypes` -Use additional TypeScript type information. Requires [typed linting](https://typescript-eslint.io/getting-started/typed-linting/) to be setup. +Enables the rule to use TypeScript type information. Requires [typed linting](https://typescript-eslint.io/getting-started/typed-linting/) to be configured. -Will automatically exclude some built-in methods such as `Map` and `Set`, and also cases where a string literal is used as a TypeScript constant: +This option automatically excludes built-in methods such as `Map` and `Set`, and cases where string literals are used as TypeScript constants, e.g.: ```ts const a: 'abc' = 'abc' ``` -### ignore +### `ignore` -The `ignore` option specifies exceptions not to check for -literal strings that match one of regexp patterns. +Specifies patterns for string literals to ignore. Strings matching any of the provided regular expressions will not trigger the rule. -Examples of correct code for the `{ "ignore": ["rgba"] }` option: +Example for `{ "ignore": ["rgba"] }`: ```jsx -/*eslint lingui/no-unlocalized-strings ["error", {"ignore": ["rgba"]}]*/ -const a =
+/*eslint lingui/no-unlocalized-strings: ["error", {"ignore": ["rgba"]}]*/ +const color =
``` -### ignoreFunction +### `ignoreFunction` -The `ignoreFunction` option specifies exceptions not check for -function calls whose names match one of regexp patterns. +Specifies functions whose string arguments should be ignored. -Examples of correct code for the `{ "ignoreFunction": ["showIntercomMessage"] }` option: +Example of `correct` code with this option: ```js -/*eslint lingui/no-unlocalized-strings: ["error", { "ignoreFunction": ["showIntercomMessage"] }]*/ -const bar = showIntercomMessage('Please, write me') +/*eslint lingui/no-unlocalized-strings: ["error", {"ignoreFunction": ["showIntercomMessage"]}]*/ +showIntercomMessage('Please write me') /*eslint lingui/no-unlocalized-strings: ["error", { "ignoreFunction": ["cva"] }]*/ const labelVariants = cva('text-form-input-content-helper', { @@ -51,53 +49,155 @@ const labelVariants = cva('text-form-input-content-helper', { }) ``` -### ignoreAttribute +This option also supports member expressions. Example for `{ "ignoreFunction": ["console.log"] }`: -The `ignoreAttribute` option specifies exceptions not to check for JSX attributes that match one of ignored attributes. +```js +/*eslint lingui/no-unlocalized-strings: ["error", {"ignoreFunction": ["console.log"]}]*/ +console.log('Log this message') +``` + +> **Note:** Only single-level patterns are supported. For instance, `foo.bar.baz` will not be matched. + +### `ignoreAttribute` -Examples of correct code for the `{ "ignoreAttribute": ["style"] }` option: +Specifies JSX attributes that should be ignored. By default, the attributes `className`, `styleName`, `type`, `id`, `width`, and `height` are ignored. + +Example for `{ "ignoreAttribute": ["style"] }`: ```jsx -/*eslint lingui/no-unlocalized-strings: ["error", { "ignoreAttribute": ["style"] }]*/ +/*eslint lingui/no-unlocalized-strings: ["error", {"ignoreAttribute": ["style"]}]*/ const element =
``` -By default, the following attributes are ignored: `className`, `styleName`, `type`, `id`, `width`, `height` +#### `regex` + +Defines regex patterns for ignored attributes. + +Example: + +```json +{ + "no-unlocalized-strings": [ + "error", + { + "ignoreAttribute": [ + { + "regex": { + "pattern": "classname", + "flags": "i" + } + } + ] + } + ] +} +``` -### strictAttribute +Example of **correct** code: -The `strictAttribute` option specifies JSX attributes which will always be checked regardless of `ignore` -option or any built-in exceptions. +```jsx +const element =
+``` + +### `strictAttribute` -Examples of incorrect code for the `{ "strictAttribute": ["alt"] }` option: +Specifies JSX attributes that should always be checked, regardless of other `ignore` settings or defaults. + +Example for `{ "strictAttribute": ["alt"] }`: ```jsx -/*eslint lingui/no-unlocalized-strings: ["error", { "strictAttribute": ["alt"] }]*/ -const element =
+/*eslint lingui/no-unlocalized-strings: ["error", {"strictAttribute": ["alt"]}]*/ +const element = IMAGE ``` -### ignoreProperty +#### `regex` + +Defines regex patterns for attributes that must always be checked. + +Example: + +```json +{ + "no-unlocalized-strings": [ + "error", + { + "strictAttribute": [ + { + "regex": { + "pattern": "^desc.*" + } + } + ] + } + ] +} +``` -The `ignoreProperty` option specifies property names not to check. +Examples of **incorrect** code: -Examples of correct code for the `{ "ignoreProperty": ["myProperty"] }` option: +```jsx +const element =
+``` + +### `ignoreProperty` + +Specifies object property names whose values should be ignored. By default, UPPERCASED properties and `className`, `styleName`, `type`, `id`, `width`, `height`, and `displayName` are ignored. + +Example for `{ "ignoreProperty": ["myProperty"] }`: ```jsx -const test = { myProperty: 'This is ignored' } -object.MyProperty = 'This is ignored' +const obj = { myProperty: 'Ignored value' } +obj.myProperty = 'Ignored value' + +class MyClass { + myProperty = 'Ignored value' +} +``` + +#### `regex` + +Defines regex patterns for ignored properties. + +Example: + +```json +{ + "no-unlocalized-strings": [ + "error", + { + "ignoreProperty": [ + { + "regex": { + "pattern": "classname", + "flags": "i" + } + } + ] + } + ] +} ``` -By default, the following properties are ignored: `className`, `styleName`, `type`, `id`, `width`, `height`, `displayName` +Examples of **correct** code: -### ignoreMethodsOnTypes +```jsx +const obj = { wrapperClassName: 'Ignored value' } +obj.wrapperClassName = 'Ignored value' -Leverage the power of TypeScript to exclude methods defined on specific types. +class MyClass { + wrapperClassName = 'Ignored value' +} +``` -Note: You must set `useTsTypes: true` to use this option. +### `ignoreMethodsOnTypes` -The method to be excluded is defined as a `Type.method`. The type and method match by name here. +Uses TypeScript type information to ignore methods defined on specific types. -Examples of correct code for the `{ "ignoreMethodsOnTypes": ["Foo.bar"], "useTsTypes": true }` option: +Requires `useTsTypes: true`. + +Specify methods as `Type.method`, where both the type and method are matched by name. + +Example for `{ "ignoreMethodsOnTypes": ["Foo.get"], "useTsTypes": true }`: ```ts interface Foo { @@ -105,8 +205,7 @@ interface Foo { } const foo: Foo - -foo.get('string with a spaces') +foo.get('Some string') ``` The following methods are ignored by default: `Map.get`, `Map.has`, `Set.has`. diff --git a/src/helpers.ts b/src/helpers.ts index 732b27a..1c8a0e0 100644 --- a/src/helpers.ts +++ b/src/helpers.ts @@ -29,8 +29,10 @@ export const LinguiCallExpressionMessageQuery = */ export const LinguiTransQuery = 'JSXElement[openingElement.name.name=Trans]' +export const UpperCaseRegexp = /^[A-Z_-]+$/ + export function isUpperCase(str: string) { - return /^[A-Z_-]+$/.test(str) + return UpperCaseRegexp.test(str) } export function isNativeDOMTag(str: string) { @@ -117,3 +119,27 @@ export function getIdentifierName(jsxTagNameExpression: TSESTree.JSXTagNameExpre return null } } + +export function isLiteral(node: TSESTree.Node | undefined): node is TSESTree.Literal { + return node?.type === TSESTree.AST_NODE_TYPES.Literal +} + +export function isTemplateLiteral( + node: TSESTree.Node | undefined, +): node is TSESTree.TemplateLiteral { + return node?.type === TSESTree.AST_NODE_TYPES.TemplateLiteral +} + +export function isIdentifier(node: TSESTree.Node | undefined): node is TSESTree.Identifier { + return (node as TSESTree.Node)?.type === TSESTree.AST_NODE_TYPES.Identifier +} + +export function isMemberExpression( + node: TSESTree.Node | undefined, +): node is TSESTree.MemberExpression { + return (node as TSESTree.Node)?.type === TSESTree.AST_NODE_TYPES.MemberExpression +} + +export function isJSXAttribute(node: TSESTree.Node | undefined): node is TSESTree.JSXAttribute { + return (node as TSESTree.Node)?.type === TSESTree.AST_NODE_TYPES.JSXAttribute +} diff --git a/src/rules/no-single-variables-to-translate.ts b/src/rules/no-single-variables-to-translate.ts index ed093af..76ee7f5 100644 --- a/src/rules/no-single-variables-to-translate.ts +++ b/src/rules/no-single-variables-to-translate.ts @@ -2,6 +2,7 @@ import { TSESTree } from '@typescript-eslint/utils' import { getText, + isJSXAttribute, LinguiCallExpressionMessageQuery, LinguiTaggedTemplateExpressionMessageQuery, } from '../helpers' @@ -49,7 +50,7 @@ export const rule = createRule({ 'JSXElement[openingElement.name.name=Trans]'(node: TSESTree.JSXElement) { const hasIdProperty = node.openingElement.attributes.find( - (attr) => attr.type === 'JSXAttribute' && attr.name.name === 'id', + (attr) => isJSXAttribute(attr) && attr.name.name === 'id', ) !== undefined if (!hasSomeJSXTextWithContent(node.children) && !hasIdProperty) { diff --git a/src/rules/no-unlocalized-strings.ts b/src/rules/no-unlocalized-strings.ts index e421b6b..c50cc27 100644 --- a/src/rules/no-unlocalized-strings.ts +++ b/src/rules/no-unlocalized-strings.ts @@ -1,33 +1,97 @@ -import { ESLintUtils, TSESTree, ParserServicesWithTypeInformation } from '@typescript-eslint/utils' import { - isUpperCase, - isAllowedDOMAttr, - getNearestAncestor, - hasAncestorWithName, + ESLintUtils, + JSONSchema, + ParserServicesWithTypeInformation, + TSESTree, +} from '@typescript-eslint/utils' +import { getIdentifierName, + getNearestAncestor, getText, + hasAncestorWithName, + isAllowedDOMAttr, + isIdentifier, + isJSXAttribute, + isLiteral, + isMemberExpression, + isTemplateLiteral, + isUpperCase, + UpperCaseRegexp, } from '../helpers' import { createRule } from '../create-rule' +type MatcherDef = string | { regex: { pattern: string; flags?: string } } + export type Option = { ignore?: string[] ignoreFunction?: string[] - ignoreAttribute?: string[] - strictAttribute?: string[] - ignoreProperty?: string[] + ignoreAttribute?: MatcherDef[] + strictAttribute?: MatcherDef[] + ignoreProperty?: MatcherDef[] ignoreMethodsOnTypes?: string[] useTsTypes?: boolean } + +const MatcherSchema: JSONSchema.JSONSchema4 = { + oneOf: [ + { + type: 'string', + }, + { + type: 'object', + properties: { + regex: { + type: 'object', + properties: { + pattern: { + type: 'string', + }, + flags: { + type: 'string', + }, + }, + required: ['pattern'], + additionalProperties: false, + }, + }, + required: ['regex'], + additionalProperties: false, + }, + ], +} + +function preparePatterns(items: MatcherDef[]): (string | RegExp)[] { + return items.map((item) => + typeof item === 'string' ? item : new RegExp(item.regex.pattern, item.regex.flags), + ) +} + +function createMatcher(patterns: (string | RegExp)[]) { + return (str: string) => { + return patterns.some((pattern) => { + if (typeof pattern === 'string') { + return pattern === str + } + + return pattern.test(str) + }) + } +} + export const name = 'no-unlocalized-strings' export const rule = createRule({ name, meta: { docs: { - description: 'disallow literal string', + description: + 'Ensures all strings, templates, and JSX text are properly wrapped with ``, `t`, or `msg` for translation.', recommended: 'error', }, messages: { - default: '{{ message }}', + default: 'String not marked for translation. Wrap it with t``, , or msg``.', + forJsxText: 'String not marked for translation. Wrap it with .', + forAttribute: + 'Attribute not marked for translation. \n Wrap it with t`` from useLingui() macro hook.', }, schema: [ { @@ -53,21 +117,15 @@ export const rule = createRule({ }, ignoreAttribute: { type: 'array', - items: { - type: 'string', - }, + items: MatcherSchema, }, strictAttribute: { type: 'array', - items: { - type: 'string', - }, + items: MatcherSchema, }, ignoreProperty: { type: 'array', - items: { - type: 'string', - }, + items: MatcherSchema, }, useTsTypes: { type: 'boolean', @@ -97,7 +155,6 @@ export const rule = createRule({ ].map((item) => new RegExp(item)) const calleeWhitelists = generateCalleeWhitelists(option) - const message = 'disallow literal string' //---------------------------------------------------------------------- // Helpers //---------------------------------------------------------------------- @@ -118,10 +175,7 @@ export const rule = createRule({ }: TSESTree.CallExpression | TSESTree.NewExpression): boolean { switch (callee.type) { case TSESTree.AST_NODE_TYPES.MemberExpression: { - if ( - callee.property.type === TSESTree.AST_NODE_TYPES.Identifier && - callee.object.type === TSESTree.AST_NODE_TYPES.Identifier - ) { + if (isIdentifier(callee.property) && isIdentifier(callee.object)) { if (calleeWhitelists.simple.includes(callee.property.name)) { return true } @@ -134,7 +188,7 @@ export const rule = createRule({ } // use power of TS compiler to exclude call on specific types, such Map.get, Set.get and so on - if (tsService && callee.property.type === TSESTree.AST_NODE_TYPES.Identifier) { + if (tsService && isIdentifier(callee.property)) { for (const ignore of ignoredMethodsOnTypes) { const [type, method] = ignore.split('.') @@ -151,15 +205,11 @@ export const rule = createRule({ return false } case TSESTree.AST_NODE_TYPES.Identifier: { - if (callee.name === 'require') { - return true - } return calleeWhitelists.simple.includes(callee.name) } case TSESTree.AST_NODE_TYPES.CallExpression: { return ( - (callee.callee.type === TSESTree.AST_NODE_TYPES.MemberExpression || - callee.callee.type === TSESTree.AST_NODE_TYPES.Identifier) && + (isMemberExpression(callee.callee) || isIdentifier(callee.callee)) && isValidFunctionCall(callee) ) } @@ -168,11 +218,10 @@ export const rule = createRule({ } } - const ignoredClassProperties = ['displayName'] const ignoredJSXElements = ['Trans'] const ignoredJSXSymbols = ['←', ' ', '·'] - const strictAttributes = [...(option?.strictAttribute || [])] + const strictAttributes = [...preparePatterns(option?.strictAttribute || [])] const ignoredAttributes = [ 'className', @@ -182,7 +231,7 @@ export const rule = createRule({ 'width', 'height', - ...(option?.ignoreAttribute || []), + ...preparePatterns(option?.ignoreAttribute || []), ] const ignoredMethodsOnTypes = [ @@ -200,7 +249,8 @@ export const rule = createRule({ 'width', 'height', 'displayName', - ...(option?.ignoreProperty || []), + UpperCaseRegexp, + ...preparePatterns(option?.ignoreProperty || []), ] //---------------------------------------------------------------------- @@ -212,6 +262,10 @@ export const rule = createRule({ return ignoredJSXSymbols.some((name) => name === str) } + const isIgnoredAttribute = createMatcher(ignoredAttributes) + const isIgnoredProperty = createMatcher(ignoredProperties) + const isStrictAttribute = createMatcher(strictAttributes) + function isIgnoredJSXElement( node: TSESTree.Literal | TSESTree.TemplateLiteral | TSESTree.JSXText, ) { @@ -240,15 +294,20 @@ export const rule = createRule({ const processTextNode = ( node: TSESTree.Literal | TSESTree.TemplateLiteral | TSESTree.JSXText, - text: string, ) => { visited.add(node) + const text = getText(node) if (!text || match(text) || isIgnoredJSXElement(node) || isIgnoredSymbol(text)) { return } - context.report({ node, messageId: 'default', data: { message } }) + if (node.type === TSESTree.AST_NODE_TYPES.JSXText) { + context.report({ node, messageId: 'forJsxText' }) + return + } + + context.report({ node, messageId: 'default' }) } const visitor: { @@ -270,19 +329,19 @@ export const rule = createRule({ }, JSXText(node: TSESTree.JSXText) { - processTextNode(node, `${node.value}`.trim()) + processTextNode(node) }, 'JSXElement > Literal'(node: TSESTree.Literal) { - processTextNode(node, `${node.value}`.trim()) + processTextNode(node) }, 'JSXElement > JSXExpressionContainer > Literal'(node: TSESTree.Literal) { - processTextNode(node, `${node.value}`.trim()) + processTextNode(node) }, 'JSXElement > JSXExpressionContainer > TemplateLiteral'(node: TSESTree.TemplateLiteral) { - processTextNode(node, getText(node)) + processTextNode(node) }, 'JSXAttribute :matches(Literal,TemplateLiteral)'( @@ -294,14 +353,14 @@ export const rule = createRule({ ) const attrName = getAttrName(parent?.name?.name) - if (strictAttributes.includes(attrName)) { + if (isStrictAttribute(attrName)) { visited.add(node) - context.report({ node, messageId: 'default', data: { message } }) + context.report({ node, messageId: 'default' }) return } // allow - if (ignoredAttributes.includes(attrName)) { + if (isIgnoredAttribute(attrName)) { visited.add(node) return } @@ -312,11 +371,11 @@ export const rule = createRule({ ) const tagName = getIdentifierName(jsxElement?.name) const attributeNames = jsxElement?.attributes.map( - (attr) => - attr.type === TSESTree.AST_NODE_TYPES.JSXAttribute && getAttrName(attr?.name?.name), + (attr) => isJSXAttribute(attr) && getAttrName(attr.name.name), ) if (isAllowedDOMAttr(tagName, attrName, attributeNames)) { visited.add(node) + return } }, @@ -334,11 +393,10 @@ export const rule = createRule({ parent.type === TSESTree.AST_NODE_TYPES.PropertyDefinition || //@ts-ignore parent.type === 'ClassProperty') && - parent.key.type === TSESTree.AST_NODE_TYPES.Identifier + isIdentifier(parent.key) && + isIgnoredProperty(parent.key.name) ) { - if (parent?.key && ignoredClassProperties.includes(parent.key.name)) { - visited.add(node) - } + visited.add(node) } }, @@ -349,55 +407,31 @@ export const rule = createRule({ 'VariableDeclarator > :matches(Literal,TemplateLiteral)'( node: TSESTree.Literal | TSESTree.TemplateLiteral, ) { + const parent = node.parent as TSESTree.VariableDeclarator + // allow statements like const A_B = "test" - if ( - node.parent.type === TSESTree.AST_NODE_TYPES.VariableDeclarator && - node.parent.id.type === TSESTree.AST_NODE_TYPES.Identifier && - isUpperCase(node.parent.id.name) - ) { + if (isIdentifier(parent.id) && isUpperCase(parent.id.name)) { visited.add(node) } }, 'Property > :matches(Literal,TemplateLiteral)'( node: TSESTree.Literal | TSESTree.TemplateLiteral, ) { - const { parent } = node - - if (parent.type === TSESTree.AST_NODE_TYPES.Property) { - // if node is key of property, skip - if (parent?.key === node) { - visited.add(node) - } + const parent = node.parent as TSESTree.Property - // name if key is Identifier; value if key is Literal - // dont care whether if this is computed or not - if ( - parent?.key?.type === TSESTree.AST_NODE_TYPES.Identifier && - (isUpperCase(parent?.key?.name) || ignoredProperties.includes(parent?.key?.name)) - ) { - visited.add(node) - } - - if ( - parent?.key?.type === TSESTree.AST_NODE_TYPES.Literal && - isUpperCase(`${parent?.key?.value}`) - ) { - visited.add(node) - } - - if ( - parent?.value?.type === TSESTree.AST_NODE_TYPES.Literal && - isUpperCase(`${parent?.value?.value}`) - ) { - visited.add(node) - } + // {A_B: "hello world"}; + // ^^^^ + if (isIdentifier(parent.key) && isIgnoredProperty(parent.key.name)) { + visited.add(node) + } - if ( - parent?.key?.type === TSESTree.AST_NODE_TYPES.TemplateLiteral && - isUpperCase(getText(parent?.key)) - ) { - visited.add(node) - } + // {["A_B"]: "hello world"}; + // ^^^^ + if ( + (isLiteral(parent.key) || isTemplateLiteral(parent.key)) && + isIgnoredProperty(getText(parent.key)) + ) { + visited.add(node) } }, 'MemberExpression[computed=true] > :matches(Literal,TemplateLiteral)'( @@ -413,8 +447,8 @@ export const rule = createRule({ const memberExp = assignmentExp.left as TSESTree.MemberExpression if ( !memberExp.computed && - memberExp.property.type === TSESTree.AST_NODE_TYPES.Identifier && - ignoredProperties.includes(memberExp.property.name) + isIdentifier(memberExp.property) && + isIgnoredProperty(memberExp.property.name) ) { visited.add(node) } @@ -501,7 +535,7 @@ export const rule = createRule({ } } - context.report({ node, messageId: 'default', data: { message } }) + context.report({ node, messageId: 'default' }) }, 'TemplateLiteral:exit'(node: TSESTree.TemplateLiteral) { if (visited.has(node)) return @@ -510,7 +544,7 @@ export const rule = createRule({ if (match(quasisValue) || !isStrMatched(quasisValue)) return - context.report({ node, messageId: 'default', data: { message } }) + context.report({ node, messageId: 'default' }) }, } @@ -548,19 +582,21 @@ const popularCallee = [ 'indexOf', 'endsWith', 'startsWith', + 'require', ] function generateCalleeWhitelists(option: Option) { - const ignoreFunction = (option && option.ignoreFunction) || [] const result = { simple: ['t', 'plural', 'select', ...popularCallee], complex: ['i18n._'], } - ignoreFunction.forEach((item: string) => { + + ;(option?.ignoreFunction || []).forEach((item) => { if (item.includes('.')) { result.complex.push(item) } else { result.simple.push(item) } }) + return result } diff --git a/tests/src/rules/no-unlocalized-strings.test.ts b/tests/src/rules/no-unlocalized-strings.test.ts index 98ef328..6b96e7a 100644 --- a/tests/src/rules/no-unlocalized-strings.test.ts +++ b/tests/src/rules/no-unlocalized-strings.test.ts @@ -16,8 +16,7 @@ const ruleTester = new RuleTester({ }, }) -const message = 'disallow literal string' -const errors = [{ messageId: 'default', data: { message } }] // default errors +const errors = [{ messageId: 'default' }] // default errors ruleTester.run(name, rule, { valid: [ @@ -134,6 +133,10 @@ ruleTester.run(name, rule, { { code: '`, filename: 'a.tsx', - errors, + errors: [{ messageId: 'forJsxText' }], }, { code: ``, filename: 'a.tsx', - errors, + errors: [{ messageId: 'forJsxText' }], }, { code: "function Button({ t= 'Name' }: {t: 'name' & 'Abs'}){} ", @@ -405,10 +415,7 @@ tsTester.run('no-unlocalized-strings', rule, { ? 'Search' : 'Search for accounts, merchants, and more...' }`, - errors: [ - { messageId: 'default', data: { message } }, - { messageId: 'default', data: { message } }, - ], + errors: [{ messageId: 'default' }, { messageId: 'default' }], }, ], })